何となく Blog by Jitta
Microsoft .NET 考

目次

Blog 利用状況
  • 投稿数 - 761
  • 記事 - 18
  • コメント - 35955
  • トラックバック - 222
ニュース
  • IE7以前では、表示がおかしい。div の解釈に問題があるようだ。
    IE8の場合は、「互換」表示を OFF にしてください。
  • 検索エンジンで来られた方へ:
    お望みの情報は見つかりましたか? よろしければ、コメント欄にどのような情報を探していたのか、ご記入ください。
It's ME!
  • はなおか じった
  • 世界遺産の近くに住んでます。
  • Microsoft MVP for Visual Developer ASP/ASP.NET 10, 2004 - 9, 2011
広告

記事カテゴリ

書庫

日記カテゴリ

ギャラリ

その他

わんくま同盟

同郷

 

ネタもと:C#をつつく26-引数の参照渡し。時には曖昧にしておくのがベスト。

他の記事を探していたら、気になる記事が見つかったので指摘してみた。でも、なんか、もう、疲れた。


値渡しの場合は、「この原価計算報告書を部長に渡してね」と直接対象を指して言っているのと同じなんだ。一方参照渡しの場合は、「この書類部長に渡してね」と間接的に対象を指しているということなんだ。つまり、この2つの言い方の違いは、 替えが可能かどうかなんだ。 先ほどの例で言うと、原価計算報告書と違う書類を渡した時、値引き渡しの場合は「おいおい○○。これ原価計算報告書じゃないぞ。」と注意されるのに対して、参照引き渡しの場合は何も言われない。だって「書類」って曖昧に言っているからね。

むぅ。。。わかりません。。。

"C 言語"のたいていの解説本では、値渡しの場合は「値がコピーされて渡される」と説明されています。また、参照渡しの場合は、「値を格納しているアドレスが渡される」と説明されています。それがどういうことかというと、コピーされたものを変更しても原本は変更されないが、アドレスを渡した場合にアドレスの先を変更すると、原本も変更されるということです。

「この書類、直しておいて」と渡すときに、コピーを渡すか、書類の名前を渡すかの違い。コピーを渡した場合、書類そのものをすぐに変更できるけれども、原本は変更されていない。書類の名前を渡した場合、言いつかった人は書類の名前から、書類を探し出さなければならない(アドレスの先へアクセスする)。そして、それは原本なので、原本が変更される。

メールの添付ファイルで「この書類、直しておいて」と送る。受け取った人がファイルの内容を修正しても、送った人が持っているファイルの内容は修正されない。これが値渡し。メールに、「ここにある書類、直しておいて」と、ファイル サーバーにあるファイル名を指定して送る。メールを受け取った人が指定されたファイルを修正すると、メールを送った人が参照するときには修正されている。これが参照渡し。

まぁ、インドリさんが説明したいものの主体がよくわからないので、間違いとはいいません。違和感を覚えます。私の説明の主体は、渡された先で変更されるものです。つまり、引き渡した先のメソッドで変更する必要があるなら参照渡し。必要がないなら値渡し、です。でもこれは、"C 言語"の話です。C# では、ちぃと事情が違ってきます。その辺は、後で。


で、コード。こちらにも引用してきます。


using System;
using System.Diagnostics;

namespace RefParameter
{
    class Program
    {
        static void Main( string[ ] args ) {
            Stopwatch watch = new Stopwatch( );
            const int max = 1000000;
            double result = 0;

            //値引渡し
            watch.Start( );
            for(int i = 0; i < max; i++) {
                result = Calculate( i );
                //resultを使って何かの計算をする
            }
            watch.Stop();
            Console.WriteLine(
                "値引渡し時の実行時間は{0}ミリ秒です。",
                watch.ElapsedMilliseconds );

            //参照引渡し
            watch.Reset( );
            watch.Start( );
            for ( double i = 0; i < max; i++ ) {
                result = i;
                RefCalculate( ref result );
                //resultを使って何かの計算をする
            }
            watch.Stop( );
            Console.WriteLine(
                "参照引渡し時の実行時間は{0}ミリ秒です。",
                watch.ElapsedMilliseconds );
        }

        private static double Calculate( int value ) {
            return value * DateTime.Now.Ticks;
        }

        private static void RefCalculate( ref double value ) {
            value *= DateTime.Now.Ticks;
        }
    }
}

どう?値引渡しと参照引渡しどちらが速かった?この状況で参照引き渡しの方が速い理由は 同じ変数を使いまわせるからなんだ。 値引渡しの場合は実行環境が毎回値を作成しているから、何度も使いまわしする場合にはスピードが落ちるんだ。

ん?そうなの?本当に実行時間が変わるの?実行してみる。

Debug>ConsoleApplication1.exe
値引渡し時の実行時間は294ミリ秒です。
参照引渡し時の実行時間は264ミリ秒です。

Debug>ConsoleApplication1.exe
値引渡し時の実行時間は295ミリ秒です。
参照引渡し時の実行時間は266ミリ秒です。

Debug>ConsoleApplication1.exe
値引渡し時の実行時間は296ミリ秒です。
参照引渡し時の実行時間は265ミリ秒です。

Debug>ConsoleApplication1.exe
値引渡し時の実行時間は299ミリ秒です。
参照引渡し時の実行時間は264ミリ秒です。

なるほど。参照渡しの方が、100万回の繰り返しで、30ミリ秒ほど速いようです。それにしても、間接的に対象を指しているということと、同じ変数を使いまわせるからが、どう繋がるのか、よくわかりませんが。まぁ、それは説明の主体の問題なので、置いておきましょう。

じゃぁ、どうして私は、実行した上で、ブログのエントリーを書いているのでしょう?私はいったい、何に違和感を持っているのでしょう?

それは、この記事が、C# を使って書かれていることです。

C# では、ほとんどのクラスは、参照型です。参照型って、なんでしょう?先の私の説明では、ファイル サーバーにファイルを置いて、ファイル サーバーにあるファイルを修正している状態である、ということです。

参照型に対して、値型というものがあります。これは、ローカルにファイルを置いて、それを修正すると喩えられるでしょう。他の人にファイルを渡すときは、メールの添付ファイルを使うことになります。そのため、ローカルのファイルとは別の、まったく同じ内容を他の人に送ることができます。

「後で」としたことに戻ります。C 言語では、変数は値型しかありませんでした。そのため、関数の引数に指定すると、必ず値のコピーが取られます。10kb のサイズがある構造体なら、10kb のコピー処理が発生します。アドレス渡しでは、アドレスだけがコピーされます。32bit 用にコンパイルされている実行形式では、通常2byteです。10kbyte のコピーより、2byte のコピーの方が速く終わるのは、いうまでもありません。そのためか、C++ では参照渡しが追加されています。しかし、C# では変数のほとんどは参照型です。関数の引数に値渡しとして指定しても、参照がコピーされるだけなので、クラスのサイズに関係なく一定量のコピー処理しか発生しません。したがって、C# においては、値渡しであっても、実行速度的な違いは発生しないと期待されます。

では、インドリさんが出されたコードは、どうか。int と double を使っています。この2つは値型です。したがって、この状況で参照引き渡し方が~の説明は、正しいでしょう。

それなのに、なぜまだ絡むのか。

値渡しには int を用い、参照渡しには double を用いています。int の値渡しと、double の参照渡し。同じように比較して良いの?

C# の数値型は、2005年に書いたのですが、int が32ビット、double が64ビットです。しかし、参照渡しというと、私の C 言語を元にした理解では、アドレスが渡される(.NET Framework においては正しい表現ではない)ので、それはやはり(32bit 環境の場合では)32ビットであり、同じ時間で完了するだろう、というのが私の予想です。しかし、実際には、値渡しの方が少し多めに時間がかかっています。

なぜでしょう?

やはり、int と double、型が違うからというところに落ち着きます。

さて、コードを、もう一度見直します。


private static double Calculate(int value) {
    return value * DateTime.Now.Ticks;
}

private static void RefCalculate(ref double value) {
    value *= DateTime.Now.Ticks;
}
 

DateTime.Ticks は、long 型です。Calculate メソッドでは、int から long に変換して計算し、さらに doluble 型へ変換しています(あるいは、int から double、long から double の2つ変換の後、計算)。しかし、Calculate メソッドでは、value が double 型なので、long から double へ変換して計算だけで済んでいます。

実は、この、型変換に時間がかかっているのではないか?

では、型を修正します。


using System;
using System.Diagnostics;

namespace RefParameter
{
    class Program
    {
        static void Main( string[ ] args ) {
            Stopwatch watch = new Stopwatch( );
            const int max = 1000000;
            /*double*/long result = 0;

            //値引渡し
            watch.Start( );
            for(/*int*/long i = 0; i < max; i++) {
                result = Calculate( i );
                //resultを使って何かの計算をする
            }
            watch.Stop();
            Console.WriteLine(
                "値引渡し時の実行時間は{0}ミリ秒です。",
                watch.ElapsedMilliseconds );

            //参照引渡し
            watch.Reset( );
            watch.Start( );
            for ( /*double*/long i = 0; i < max; i++ ) {
                result = i;
                RefCalculate( ref result );
                //resultを使って何かの計算をする
            }
            watch.Stop( );
            Console.WriteLine(
                "参照引渡し時の実行時間は{0}ミリ秒です。",
                watch.ElapsedMilliseconds );
        }

        private static double Calculate( /*int*/long value ) {
            return value * DateTime.Now.Ticks;
        }

        private static void RefCalculate( ref /*double*/long value ) {
            value *= DateTime.Now.Ticks;
        }
    }
}

すべて long で統一しました。これで実行してみます。

>ConsoleApplication1.exe
値引渡し時の実行時間は270ミリ秒です。
参照引渡し時の実行時間は271ミリ秒です。

>ConsoleApplication1.exe
値引渡し時の実行時間は269ミリ秒です。
参照引渡し時の実行時間は268ミリ秒です。

>ConsoleApplication1.exe
値引渡し時の実行時間は267ミリ秒です。
参照引渡し時の実行時間は270ミリ秒です。

>ConsoleApplication1.exe
値引渡し時の実行時間は269ミリ秒です。
参照引渡し時の実行時間は265ミリ秒です。

このように、有意と思われる差はなくなりました。私はわからないのですが、IL も載せておきます。ここまで書いておけば、どなたかが IL ベースで解説してくださるでしょう。


インドリさんのコード

// Calculate( int value )
.method private hidebysig static float64
        Calculate(int32 'value') cil managed
{
  // コード サイズ       23 (0x17)
  .maxstack  2
  .locals init ([0] float64 CS$1$0000,
           [1] valuetype [mscorlib]System.DateTime CS$0$0001)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  conv.i8
  IL_0003:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_0008:  stloc.1
  IL_0009:  ldloca.s   CS$0$0001
  IL_000b:  call       instance int64 [mscorlib]System.DateTime::get_Ticks()
  IL_0010:  mul
  IL_0011:  conv.r8
  IL_0012:  stloc.0
  IL_0013:  br.s       IL_0015
  IL_0015:  ldloc.0
  IL_0016:  ret
} // end of method Program::Calculate

// RefCalculate( ref double value )
.method private hidebysig static void  RefCalculate(float64& 'value') cil managed
{
  // コード サイズ       21 (0x15)
  .maxstack  3
  .locals init ([0] valuetype [mscorlib]System.DateTime CS$0$0000)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  dup
  IL_0003:  ldind.r8
  IL_0004:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_0009:  stloc.0
  IL_000a:  ldloca.s   CS$0$0000
  IL_000c:  call       instance int64 [mscorlib]System.DateTime::get_Ticks()
  IL_0011:  conv.r8
  IL_0012:  mul
  IL_0013:  stind.r8
  IL_0014:  ret
} // end of method Program::RefCalculate

おそらく、conv.r8, conv.i8 というのが、型変換だと思います。Calculate の方は2回、RefCalculate の方は1回、現れています。


// Calculate(long value)
.method private hidebysig static void  Calculate(int64 'value') cil managed
{
  // コード サイズ       19 (0x13)
  .maxstack  2
  .locals init ([0] valuetype [mscorlib]System.DateTime CS$0$0000)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_0007:  stloc.0
  IL_0008:  ldloca.s   CS$0$0000
  IL_000a:  call       instance int64 [mscorlib]System.DateTime::get_Ticks()
  IL_000f:  mul
  IL_0010:  starg.s    'value'
  IL_0012:  ret
} // end of method Program::Calculate

// RefCalculate(long ref value)
.method private hidebysig static void  RefCalculate(int64& 'value') cil managed
{
  // コード サイズ       20 (0x14)
  .maxstack  3
  .locals init ([0] valuetype [mscorlib]System.DateTime CS$0$0000)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  dup
  IL_0003:  ldind.i8
  IL_0004:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_0009:  stloc.0
  IL_000a:  ldloca.s   CS$0$0000
  IL_000c:  call       instance int64 [mscorlib]System.DateTime::get_Ticks()
  IL_0011:  mul
  IL_0012:  stind.i8
  IL_0013:  ret
} // end of method Program::RefCalculate

私は、パフォーマンスの計測は、同じ条件で行わなければならないと思っています。今、比較しようとしているのは「参照渡しと値渡しによって、パフォーマンスに差があるか」ということです。そうであるなら、「参照渡し」か、「値渡し」か、というところ以外は、同じコードでなければならいと思います。もちろん、まったく同じにできないこともあります。しかし、比較結果に差を与えるようなものは、極力排除するべきだと考えます。

この理由から、インドリさんのこれについては、後にボックス化の説明をするために、わざとこうしています。という理由には、納得できません。今(2009/05/22)のところ、C# のカテゴリにボックス化の説明が無い様に思われるので、どのような説明をしようとされているのかわかりませんが、少なくともパフォーマンスを比較するという目的に於いて、このコードは不適切だと思います。

なお、「参照渡しの方が、値渡しに比べて実行速度が速くなる」ということ自体について、間違っているわけではありません。サイズが大きな値型を用いる場合、有意な差が出てくるでしょう。今回は long にしたので、値渡しと参照渡しでコピーされる量がほぼ同じであると思われます。これを、double や decimal を使うと、また違った結果になると思われます。

原稿段階で「配列を用いると、差が出るでしょう」と書いたのですが、確認してみると、配列は抽象基本型 Array から派生した参照型(C# プログラミングガイド「配列」より)でした。よって、配列では、速度的な差はないと思われます。

decimal では、差が出ませんでした。double では、参照渡しの方が10ミリ秒ほど速くなりました。

C# に於いては、ほとんどのクラスは参照型です。このため、「値渡し」は「参照の値渡し」であり、「参照の参照渡し」と速度面での差はありません。このことについて、VB6.0以前のユーザーが、VB.NET を使うことになったときに、「メソッド引数が ByVal になっているのはなぜ」と、疑問を上げています。この疑問に対する答えを調べれば、回答となるでしょう。また、C 言語の場合は「引き渡した先の関数で、原本を変更できるかどうか」という違いがありましたが、参照を引き渡す C# では、値渡しであっても原本が変更されます(参照型の場合)。

このように、C# や VB.NET(「VB7.0 以降」の意味で VB.NET を用いています)では、メソッドの引数に参照渡しを用いる必要は、ほとんどありません。


では、ボックス化/ボックス化解除によって、差が出ているのでしょうか。まず、「ボックス化」とは、なんでしょう?

C# Language Specification 1.2 より

4.3 ボックス化とボックス化解除

ボックス化とボックス化解除は、C# の型システムの中心的な概念です。これは、value-type のすべての値と object 型間の変換を可能にするため、value-types と reference-types との橋渡しの役目を果たします。ボックス化とボックス化解除の機能により、型システムを統一的に見ることができるようになり、すべての型の値を最終的にオブジェクトとして扱うことができます。

値型と、参照型の変換で使われるようです。ソース コードを眺める限り、そのような変換をしているところは見あたりません。念のため、IL も確認します。object o; と宣言を加え、o = value; と代入させます。これを ildasm にかけると、ボックス化の IL は box であることがわかります。では、上の2つのメソッドで、ボックス化は行われているでしょうか。いません。念のため、Main メソッドも見てみます。


.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // コード サイズ       182 (0xb6)
  .maxstack  2
  .locals init ([0] class [System]System.Diagnostics.Stopwatch watch,
           [1] float64 result,
           [2] int32 i,
           [3] float64 V_3,
           [4] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  newobj     instance void [System]System.Diagnostics.Stopwatch::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldc.r8     0.0
  IL_0010:  stloc.1
  IL_0011:  ldloc.0
  IL_0012:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Start()
  IL_0017:  nop
  IL_0018:  ldc.i4.0
  IL_0019:  stloc.2
  IL_001a:  br.s       IL_0029
  IL_001c:  nop
  IL_001d:  ldloc.2
  IL_001e:  call       float64 RefParameter.Program::Calculate(int32)
  IL_0023:  stloc.1
  IL_0024:  nop
  IL_0025:  ldloc.2
  IL_0026:  ldc.i4.1
  IL_0027:  add
  IL_0028:  stloc.2
  IL_0029:  ldloc.2
  IL_002a:  ldc.i4     0xf4240
  IL_002f:  clt
  IL_0031:  stloc.s    CS$4$0000
  IL_0033:  ldloc.s    CS$4$0000
  IL_0035:  brtrue.s   IL_001c
  IL_0037:  ldloc.0
  IL_0038:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Stop()
  IL_003d:  nop
  IL_003e:  ldstr      bytearray (24 50 15 5F 21 6E 57 30 42 66 6E 30 9F 5B 4C 88   // $P._!nW0Bfn0.[L.
                                  42 66 93 95 6F 30 7B 00 30 00 7D 00 DF 30 EA 30   // Bf..o0{.0.}..0.0
                                  D2 79 67 30 59 30 02 30 )                         // .yg0Y0.0
  IL_0043:  ldloc.0
  IL_0044:  callvirt   instance int64 [System]System.Diagnostics.Stopwatch::get_ElapsedMilliseconds()
  IL_0049:  box        [mscorlib]System.Int64
  IL_004e:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_0053:  nop
  IL_0054:  ldloc.0
  IL_0055:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Reset()
  IL_005a:  nop
  IL_005b:  ldloc.0
  IL_005c:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Start()
  IL_0061:  nop
  IL_0062:  ldc.r8     0.0
  IL_006b:  stloc.3
  IL_006c:  br.s       IL_0086
  IL_006e:  nop
  IL_006f:  ldloc.3
  IL_0070:  stloc.1
  IL_0071:  ldloca.s   result
  IL_0073:  call       void RefParameter.Program::RefCalculate(float64&)
  IL_0078:  nop
  IL_0079:  nop
  IL_007a:  ldloc.3
  IL_007b:  ldc.r8     1.
  IL_0084:  add
  IL_0085:  stloc.3
  IL_0086:  ldloc.3
  IL_0087:  ldc.r8     1000000.
  IL_0090:  clt
  IL_0092:  stloc.s    CS$4$0000
  IL_0094:  ldloc.s    CS$4$0000
  IL_0096:  brtrue.s   IL_006e
  IL_0098:  ldloc.0
  IL_0099:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Stop()
  IL_009e:  nop
  IL_009f:  ldstr      bytearray (C2 53 67 71 15 5F 21 6E 57 30 42 66 6E 30 9F 5B   // .Sgq._!nW0Bfn0.[
                                  4C 88 42 66 93 95 6F 30 7B 00 30 00 7D 00 DF 30   // L.Bf..o0{.0.}..0
                                  EA 30 D2 79 67 30 59 30 02 30 )                   // .0.yg0Y0.0
  IL_00a4:  ldloc.0
  IL_00a5:  callvirt   instance int64 [System]System.Diagnostics.Stopwatch::get_ElapsedMilliseconds()
  IL_00aa:  box        [mscorlib]System.Int64
  IL_00af:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_00b4:  nop
  IL_00b5:  ret
} // end of method Program::Main

2カ所ありました。しかし、場所をよく見ると、System.Diagnostics.Stopwatch::get_ElapsedMilliseconds() の直後です。はい、WriteLine メソッドの引数が object 型なので、long 型である Stopwatch.ElapsedMilliseconds プロパティの値をボックス化しているわけです。

このことより、私は、ボックス化/ボックス化解除が実行パフォーマンスに影響を与えているとは思いません。おそらく、インドリさんの手元にあるコードでは、メソッドの引数が object 型なのでしょう。そうすると、最初の原価計算報告書と違う書類を渡した時、値引き渡しの場合は「おいおい○○。これ原価計算報告書じゃないぞ。」と注意されるのに対して、参照引き渡しの場合は何も言われない。だって「書類」って曖昧に言っているからね。も、納得がいくような気がするし、ボックス化というのも合致します。
でも、そこまで読み解くのは、本当に、疲れる。。。


さて、「要望があった」と書いてあるので、どんな要望だったのだろうと前の方に繰っていくと、中の人の徒然草42のコメントの様です。

もしよろしれば、自作関数での引数のrefに関して、何かの例えを使ってご説明いただければ有り難く思います。

むぅ。。。どういう状況での質問で、ref の、何を知りたいのか、私にはさっぱりわからないです。。。それなのに、時には曖昧にしておくのがベストとエントリーを作り、さらにボックス化/ボックス化解除のさわりまで説明してしまういんどりさんは、ほんとうにすごいとおもいます。

投稿日時 : 2009年5月22日 22:29
コメント
タイトル
名前
Url
コメント