R#は正しいですか:.ToString()の呼び出しは冗長ですか?

この投稿は、発行するのに十分なカルマを持っていないhabrayuzer mstyuraのリクエストで発行されています。 この記事が気に入ったら、著者に感謝し、カルマを手伝ってください。

重要な種類の包装/開梱のトピックに関するミニスタディの結果をKhabrosocommunityと共有したいと思います。 このトピックを書くきっかけとなったのは、リヒター「C#を介したCLR」R#そのものです。 私の意見では、後者は私のコードに「不正な」コメントを与えました。

問題は何ですか?


実際、私はこのようにかなり標準的なコードを書いた
string str = "habrahabr" ;
int val = 0;
var resultString = str + val.ToString();

* This source code was highlighted with Source Code Highlighter .

Resharperは、重要な変数valでのToString()メソッドの明示的な呼び出しを好まなかったが、この場合、書いたとおりに実行する方が確実であることがわかっているときに何をすべきかを彼が教えてくれることは当然嫌いだった。 誰が正しいか見てみましょう。 まず、R#で提案されているコードを実行したときにどのような操作が発生するかを考えておくことをお勧めします。
string str = "habrahabr" ;
int val = 0;
var resultString = str + val;

* This source code was highlighted with Source Code Highlighter .

最後の行には、異なるタイプの2つの変数を追加する操作があり、その結果は文字列になります。 変数はさまざまなタイプであるため、 文字列 Systemメソッドの次のバージョンが呼び出されます。 文字列 .Concat( オブジェクトオブジェクト )。 つまり 実際、プログラムコードは次のようになります
string str = "habrahabr" ;
int val = 0;
var resultString = System. String .Concat(str, val);

* This source code was highlighted with Source Code Highlighter .

str(文字列は参照型)およびval(整数は重要な型)パラメーターを渡すと、2番目のパラメーターに対してパッケージング操作が実行されます。これは、重要なint型をメソッドに渡そうとしているため、参照オブジェクトを予期しているためです。

パッケージとは何ですか?


正確に定義するために、Richter 「CLR via c#」に注目しました 。 だから:
ボクシングとは、重要な型を参照に変換することです。 重要なタイプのインスタンスをパッケージ化すると、次のことが発生します。
1.マネージヒープでは、メモリが割り当てられます。 そのボリュームは、有効なタイプの長さと、マネージヒープ内のすべてのオブジェクトに必要な2つの追加メンバー(オブジェクトタイプへのポインターとSyncBlockIndexインデックス)によって決まります。
2.重要なタイプのフィールドは、ヒープ上に割り当てられたメモリにコピーされます。
3.オブジェクトのアドレスが返されます。 このアドレスはオブジェクトへの参照です。 意味のある型が参照型になりました。

まだToString()を呼び出す場合


System.String.Concat()メソッドに渡す前にval変数でToString()メソッドを呼び出すと、コンパイラは文字列 System 文字列連結メソッドの次のバージョンを選択します。 String .Concat( stringstring )、なぜなら 同じ参照タイプ文字列のオブジェクトが入力に送られます。 この場合、パッキング操作は実行されません。これは、ヒープ上の重要な型にメモリを割り当て、重要な型の元の変数のすべてのバイトをそこにコピーし、割り当てられたメモリ位置へのポインタを返すことを意味します。

何にコンパイルしますか?


ToString()を呼び出さずに
  1. .locals init([0] string str、[1] int32 val、[2] string resultString)
  2. IL_0000:nop
  3. IL_0001:ldstr "habrahabr"
  4. IL_0006:stloc.0
  5. IL_0007:ldc.i4.0
  6. IL_0008:stloc.1
  7. IL_0009:ldloc.0
  8. IL_000a:ldloc.1
  9. IL_000b: ボックス [mscorlib]システム。 Int32
  10. IL_0010: 文字列 [mscorlib]システムを呼び出します文字列 ::連結( オブジェクトオブジェクト
  11. IL_0015:stloc.2
*このソースコードは、 ソースコードハイライターで強調表示されました。

ToString()を呼び出して
  1. .locals init([0] string str、[1] int32 val、[2] string resultString)
  2. IL_0000:nop
  3. IL_0001:ldstr "habrahabr"
  4. IL_0006:stloc.0
  5. IL_0007:ldc.i4.0
  6. IL_0008:stloc.1
  7. IL_0009:ldloc.0
  8. IL_000a:ldloca.s val
  9. IL_000c:呼び出しインスタンス文字列 [mscorlib]システム。 Int32 :: ToString()
  10. IL_0011:呼び出し文字列 [mscorlib]システム。 文字列 ::連結( stringstring
  11. IL_0016:stloc.2
*このソースコードは、 ソースコードハイライターで強調表示されました。

与えられたILコードの主な違いは、ボックス操作です。これは、パッキング操作を実行します。逆のアンパック命令は、アンボックス命令に対応します。ここでは考慮しません。

パフォーマンスへの影響


明らかに、上記のコードのパフォーマンスを判断することは困難です。なぜなら、 単一の梱包作業で十分に高速です。 次のループを書く
for ( int i = 0; i < 10000000; i++)
{
var s = "habrahabr" + i;
}

* This source code was highlighted with Source Code Highlighter .

ランタイムを測定するには、System.Diagnostics名前空間のStopwatchクラスを使用します。 DateTimeを使用するよりもはるかに正確な結果が得られます。 たとえば、私のマシンでは、DateTime.Nowの2つを連続して呼び出すと、差00:00:00.0010010000が得られ、 ストップウォッチの即時開始と停止は00:00:00.0000015になります。 肉眼でのWindowsの違い。
パッケージング操作をテストする最終コードは次のようになります
namespace BoxingTraining
{
using System;
using System.Diagnostics;
public class Program
{
private static void Main()
{
var time = Stopwatch.StartNew();
for ( int i = 0; i < 10000000; i++)
{
var s = "habrahabr" + i;
}
Console .WriteLine(time.Elapsed.ToString());
}
}
}

* This source code was highlighted with Source Code Highlighter .

以下は、int値(4バイト)をパックするときに上記のコードを実行した結果の表です。
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000,03244210,03148380,00095833,043788
1,000,0000.33298100.29270900,040272013,75837
10,000,0003,53443303.24258430.29184879,000497
1億35.902293735,42089820.48139551.359072

ご覧のように、結果は、パッケージを支持するものではありませんが、一度に見えるほどひどいものではありません。 さらに、型サイズ(占有メモリの値)も結果に影響を与える可能性があると想定できます。次のステップは、char型、byte型、ある種の重い重み付き構造の1つの値をパックすることです。
重要なタイプの変数のパッケージングをテストするためのコード。
namespace boxingTraining
{
using System;
using System.Diagnostics;
public class Program
{
private static void Main()
{
var time = Stopwatch.StartNew();
for ( int i = 0; i < 1000000; i++)
{
var s = "habrahabr" + ;
}
Console .WriteLine(time.Elapsed.ToString());
}
}
}

* This source code was highlighted with Source Code Highlighter .

charの結果のテーブル(2バイト)
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000,01201200,00802800,00398449,62631
1,000,0000.09255450,07386900.018685525,29546
10,000,0000.89496940.72985980.165109622.6221
1億9,19085566,99771692,193138731,34077

バイト(1バイト)の結果テーブル
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000.02643630,02422110.00221529,145745
1,000,0000.26006720.23041880,029648412,86718
10,000,0002,55634602,27130210.285043912,5498
1億25,184794422,30633522,878459212,90422

少し叙情的な余談。 このトピックを書いてこの点に到達したとき、この記事を書くときに相談したAldanko habrayuzerは 、この状況での動作を確認し、内部タイプデバイスの影響を調べるために、より標準的なタイプのテストを強く推奨しましたパッキング操作。
次のタイプのSystem.Int16(2バイト)、System.Int64(8バイト)、System.Single(4バイト)、System.Double(8バイト)、およびSystem.Decimal(16バイト)をテストします。
System.Int16の場合
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000,02682690,02527530,0015526.1388
1,000,0000.25081710.22831340,0225049.856496
10,000,0002,58401732,27711030.30690713,47792
1億25.557501422,63220242,92529912.92538

System.Int64の場合
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000,03132520,02615760.00516819,75564
1,000,0000.27304050.25202120,0210198,34029
10,000,0002,75734222.30897440.44836819,41848
1億26,687696423.35651233.33118414.26234

System.Singleの場合
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000,04882710.04351740.0053112,20133
1,000,0000.45858080.42773030,0308517,212606
10,000,0004,50099574,23132420.2696716.373218
1億44,990615442,57028072,4203355,685503

System.Doubleの場合
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000.04772930,04540060.0023295,129227
1,000,0000.47749110.45076110,026735.92997
10,000,0004.74261564,52359230.2190234.8418
1億47.481688144.33830823.143387.089535

System.Decimalの場合
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000.03422770.03023810.0039913.19395
1,000,0000,30824510.28527630,0229698.051422
10,000,0003.05176152.81427090.2374918,438797
1億30.782424127.81044802.97197610.68655

ご覧のとおり、ToString()の呼び出しは常に高速です。 原則として、遅くすることはできません。 呼び出されない場合、呼び出されますが、その前に、重要なタイプのオブジェクトはまだパックされます。

カスタム構造のパフォーマンス


私がすでにテストした最も一般的な標準タイプ。 構造を書くときが来ました。 同じhabruiser Aldankoのアドバイスに基づいて、1600バイトのタイプのフィールド(1バイト)と100フィールドの10進数のフィールド(16バイト)を含む構造を記述します。 最初の構造は次のとおりです
public struct DecimalStruct
{
public decimal Field1;
public decimal Field2;
public decimal Field3;
public decimal Field4;
...
public decimal Field97;
public decimal Field98;
public decimal Field99;
public decimal Field100;
public override string ToString()
{
return "DecimalStruct" ;
}
}

* This source code was highlighted with Source Code Highlighter .

コードの完全版をダウンロードできます。
1600バイトのフィールド構造
public struct ByteStruct
{
public byte Field1;
public byte Field2;
public byte Field3;
public byte Field4;
...
public byte Field1597;
public byte Field1598;
public byte Field1599;
public byte Field1600;
public override string ToString()
{
return "ByteStruct" ;
}
}

* This source code was highlighted with Source Code Highlighter .

コードの完全版をダウンロードできます。
したがって、パッケージング操作をテストするコードは次のようになります
namespace boxingTraining
{
using System;
using System.Diagnostics;
public class Program
{
private static void Main()
{
var myStruct = new ByteStruct(); // new DecimalStruct()
var time = Stopwatch.StartNew();

for ( int i = 0; i < 100000; i++)
{
var s = "habrahabr" + myStruct; // myStruct.ToString()
}
Console .WriteLine(time.Elapsed.ToString());
}
}
}

* This source code was highlighted with Source Code Highlighter .

これらの不正行為の目的は、構造の内部構造がパッケージング操作の時間にどのように影響するか、つまり、内部データを管理ヒープにコピーすることです。
その結果。 ByteStructの場合
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000,05538110,00727600,048105661.1476
1,000,0000.52180770.05911200.462696782,7441
10,000,0005,20838780,55200374,656384843.5422
1億61,81427505,544944856,269331014,786

DecimalStringの概要
ループ内の反復回数時間、秒違い、c。賞金、%
ToStringなし()ToString()を使用
100,0000.08156640,00685410,0747121090,038
1,000,0000.82086480,06388180.7569831184,974
10,000,0006,93492830.62493096.3099971009,711
1億54,71892056,845334947.87359699,3608

かさばる構造に関する上記のテストからわかるように、ToStringを呼び出すと、最大10倍のゲインが得られます。 これがはっきりと見えていたのは事実です。パッケージ操作を複数回繰り返す必要があります。

結論


このミニスタディからどのような結論を導き出すことができますか? 少なくとも私にとっては明らかです。 参照型の場合、ToString()を呼び出してもメリットはなく、文字列操作で使用しても冗長になりますが、重要な型の場合、ToString()を呼び出す必要があります。 呼び出されると、かなりリソースを消費する操作(パッケージング)を回避します。これは、特定の場合にコードのパフォーマンスを著しく低下させる可能性があります。 さて、R#は間違っていることが判明し、文字列の連結中の重要な型でのToString()の呼び出しは冗長であると宣誓しました。 制作することはできませんが、パフォーマンスで支払うことができます。

プログイット

Source: https://habr.com/ru/post/J70217/


All Articles