「as」および null 許容型による驚くべきパフォーマンス
-
21-09-2019 - |
質問
現在、Null 許容型を扱う『C# in Depth』の第 4 章を改訂しており、次のように記述できる "as" 演算子の使用に関するセクションを追加しています。
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
これは非常に優れており、「is」に続いてキャストを使用することで、同等の C# 1 よりもパフォーマンスが向上する可能性があると思いました。結局のところ、この方法では、動的型チェックを 1 回要求するだけで済み、その後は単純な値チェックを行うだけです。 。
しかし、そうではないようです。以下にサンプル テスト アプリを含めました。これは基本的にオブジェクト配列内のすべての整数を合計します。ただし、配列にはボックス化された整数だけでなく、多くの null 参照と文字列参照が含まれています。このベンチマークは、C# 1 で使用する必要があるコード、「as」演算子を使用するコード、および単なる LINQ ソリューションを測定します。驚いたことに、この場合、C# 1 コードは 20 倍高速であり、LINQ コード (反復子が関与していることを考えると、もっと遅いと予想していました) でさえ、「as」コードよりも高速です。
の .NET 実装です isinst
null 許容型の場合は本当に遅いのでしょうか?追加ですかね unbox.any
それが問題の原因ですか?これについて別の説明はありますか?現時点では、パフォーマンスが重視される状況でこれを使用することに対する警告を含める必要があるように感じます...
結果:
キャスト:10000000 :121
として:10000000 :2211
リンク:10000000 :2143
コード:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
}
解決
は明らかに、JITコンパイラは最初のケースのために生成することができるマシンコードは、はるかに効率的です。実際にそこに役立ちます一つのルールは、オブジェクトが唯一の箱入りの値と同じ型を持つ変数にアンボクシングすることができることです。つまり、JITコンパイラは非常に効率的なコードを生成することができ、値の変換を考慮しなければならない。
がのあるオペレータのテストでは、オブジェクトがnullでなく、予想されるタイプのものであり、かかるが、いくつかのマシンコード命令場合だけチェックし、簡単です。キャストが容易でもある、JITコンパイラは、オブジェクトの値のビットの位置を知っており、それらを直接使用しています。いかなるコピーや変換発生し、すべてのマシンコードは、インラインではありませんとなりますが、ダース命令について。これはボクシングが一般的だった.NET 1.0で、本当に効率的なバックする必要がありました。
intにキャスト?多くの手間がかかります。ボックス化整数値の表現はNullable<int>
のメモリレイアウトと互換性がありません。変換が必要であり、コードが可能箱入りenum型によるトリッキーです。 JITコンパイラは、仕事を得るためにJIT_Unbox_Nullableという名前のCLRヘルパー関数の呼び出しを生成します。これは、任意の値タイプのための汎用的な機能が、そこのコードの多くは、種類を確認することです。そして、値がコピーされます。このコードはのMscorwks.dllの内側にロックアップしますが、マシンコード命令の数百人がそうであるからである。
はLINQのOfType()拡張メソッドはまた、がのオペレータとキャストを使用しています。しかし、これはジェネリック型へのキャストです。 JITコンパイラは、任意の値型にキャストを行うことができるヘルパー関数、JIT_Unbox()への呼び出しを生成します。それはあまり仕事が必要であるべきことを考えれば、Nullable<int>
へのキャストとして遅いようですなぜ私は偉大な説明はありません。私はここでそのngen.exeかもしれない原因のトラブルを疑っています。
他のヒント
isinst
がNULL可能なタイプで本当に遅いと私には思えます。メソッドFindSumWithCast
で、私は変更
if (o is int)
と
if (o is int?)
これも大幅に実行が遅くなります。私が見ることができるILで唯一differencは
ということですisinst [mscorlib]System.Int32
に変更されます。
isinst valuetype [mscorlib]System.Nullable`1<int32>
このためコメントをハンス-Passantの優れた回答だった長していきたいなと思っていますのも、ほかのビットはこちら
第一に、C# as
オペレーターを発する isinst
IL説明書(でも is
オペレーター).(もう一つの興味深い指導を castclass
,emitedだけど、直接キャストのコンパイラを知ってるランタイムで確認できないommited.)
これがその isinst
は(ECMA335パIII4.6):
フォーマット: isinst typeTok
typeTok はメタデータのトークン(
typeref
,typedef
またはtypespec
が示唆されていることから、希望のクラスです。の場合 typeTok は非null値の型は汎用のパラメータの型であるとして解釈される"弁当" typeTok.
の場合 typeTok はnullable型
Nullable<T>
, であるとして解釈される"弁当"T
最も重要なのは:
場合に実際の型は、検証者を追跡型) obj は 検証者-割当可能に タイプtypeTokし
isinst
に成功し、 obj ( 結果)を変更せずに返し検証トラック型として typeTok. とは異なりcoercions(§1.6)および変換(§3.27),isinst
変わることはありません実際の型のオブジェクト保存するとともにオブジェクトアイデンティティ参照の分配という。)
ることは可能でキラーな isinst
この場合、追加 unbox.any
.このなかのハンス'の答えは、彼の場合の近似式は以下のようになJITedコードのみです。一般のC#コンパイラを発する unbox.any
した後、 isinst T?
(ものを省略するとい isinst T
, 時 T
は参考タイプ)です。
なぜなる。 isinst T?
などに効果があるとって自明であったかも知れる返されますが、 T?
.代わりに、これらすべての指示を確保している人が、 "boxed T"
できるunboxedる T?
.得実績 T?
, まだunbox社 "boxed T"
へ T?
, のコンパイラを放 unbox.any
後 isinst
.考えてみれば、この意味での"ボックスフォーマット"のための T?
は "boxed T"
決 castclass
や isinst
を行うunboxうに統一性がない。
バックアップハンスの発見と情報の 標準, こちらでは:
(ECMA335パIII、4.33): unbox.any
適用される場合には箱の値型を、
unbox.any
指導を抽出し、値に含まれるobjの型O
).(相当するものでunbox
次いでldobj
.) 適用される場合には、参照型のunbox.any
指導には、以下と同じ効果がありますcastclass
typeTok.
(ECMA335パIII4.32): unbox
通常、
unbox
単なる計算のアドレス値の型が存在してい内食のオブジェクトです。このアプローチできない場合unboxing nullable値です。のでNullable<T>
値に変換され弁当Ts
の操作ボックス実装では、しばしば必ず新規に製作Nullable<T>
のヒープおよび計算の住所に新たに割り当てオブジェクトです。
興味深いことに、オペレーターのサポートに関するフィードバックを次の方法で送信しました。 dynamic
桁違いに遅い Nullable<T>
(に似ている この初期のテスト) - 非常に似たような理由があるのではないかと思います。
お奨めの愛 Nullable<T>
. 。もう 1 つの興味深い点は、JIT がスポット (および削除) を実行しても、 null
null 不可の構造体の場合は、それを実行します。 Nullable<T>
:
using System;
using System.Diagnostics;
static class Program {
static void Main() {
// JIT
TestUnrestricted<int>(1,5);
TestUnrestricted<string>("abc",5);
TestUnrestricted<int?>(1,5);
TestNullable<int>(1, 5);
const int LOOP = 100000000;
Console.WriteLine(TestUnrestricted<int>(1, LOOP));
Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
Console.WriteLine(TestNullable<int>(1, LOOP));
}
static long TestUnrestricted<T>(T x, int loop) {
Stopwatch watch = Stopwatch.StartNew();
int count = 0;
for (int i = 0; i < loop; i++) {
if (x != null) count++;
}
watch.Stop();
return watch.ElapsedMilliseconds;
}
static long TestNullable<T>(T? x, int loop) where T : struct {
Stopwatch watch = Stopwatch.StartNew();
int count = 0;
for (int i = 0; i < loop; i++) {
if (x != null) count++;
}
watch.Stop();
return watch.ElapsedMilliseconds;
}
}
結果:
を使用
as
, での試験の場合はオブジェクトのインスタンスであるInt32;の下にフードを使用しておりますのでisinst Int32
(とよく似た手書きのコード:if(o int)).を使用as
, でも無条件にunboxのオブジェクトです。でリアルなパフォーマンス-キラーと呼性(機能のフード),IL_0027をキャストは、試験の場合はオブジェクトが
int
if (o is int)
;ボンネットの下にはこの使用isinst Int32
.の場合はインスタンスのintきますので、その安全にunboxの価値IL_002D
簡単に言うと、この擬似コードを使用 as
アプローチ:
int? x;
(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)
if (x.HasValue)
sum += x.Value;
この擬似コードのキャストアプローチ:
if (o isinst Int32)
sum += (o unbox Int32)
そのキャスト((int)a[i]
, もの書式のように見えだが、これは実際unboxing、キャストとunboxingでは、同じ構文を共有します、おすすめするpedanticの用語のアプローチは本当に速いだけに必要なunbox値があるオブジェクトの評価を得てき int
.同じことをできないと利用 as
ます。
プロファイリングさ:
using System;
using System.Diagnostics;
class Program
{
const int Size = 30000000;
static void Main(string[] args)
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i + 1] = "";
values[i + 2] = 1;
}
FindSumWithIsThenCast(values);
FindSumWithAsThenHasThenValue(values);
FindSumWithAsThenHasThenCast(values);
FindSumWithManualAs(values);
FindSumWithAsThenManualHasThenValue(values);
Console.ReadLine();
}
static void FindSumWithIsThenCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int)o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Is then Cast: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenHasThenValue(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As then Has then Value: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenHasThenCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += (int)o;
}
}
sw.Stop();
Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithManualAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
bool hasValue = o is int;
int x = hasValue ? (int)o : 0;
if (hasValue)
{
sum += x;
}
}
sw.Stop();
Console.WriteLine("Manual As: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenManualHasThenValue(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (o is int)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
}
出力:
Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282
何ができる推定したさらにこれらの数値は?
- 最初に、そしてキャストアプローチが有意に高速 として ます。303vs3524
- 第二に、.値が小幅より遅くなります。3524vs3272
- 第三に、.HasValueは小幅より遅くな利用マニュアル(を使用 は).3524vs3282
- 第四に、やっているファミマクレジットとの比較(両方の手配を模擬HasValueに転換する模擬値が起こるとと シミュレーション や リアルとして アプローチまで シミュレーション もく リアルとして.395vs3524
- 最後に、最初と最後にあるように として 実施^_^
この回答を最新の状態に保つために、このページでの議論のほとんどは現在議論の余地がないことに言及する価値があります。 C# 7.1 そして .NET 4.7 これは、最適な IL コードも生成するスリムな構文をサポートします。
OPの元の例...
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
// ...use x.Value in here
}
単純になります...
if (o is int x)
{
// ...use x in here
}
新しい構文の一般的な用途の 1 つは、.NET を作成するときであることがわかりました。 値の型 (すなわち、 struct
で C#) を実装する IEquatable<MyStruct>
(ほとんどの場合そうであるように)。強く型付けされたメソッドを実装した後、 Equals(MyStruct other)
メソッドを使用すると、型なしのメソッドを適切にリダイレクトできるようになりました。 Equals(Object obj)
オーバーライド (から継承 Object
) 次のように変更します。
public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
付録: の Release
建てる イリノイ州 この回答で上に示した最初の 2 つの関数例のコード (それぞれ) をここに示します。新しい構文の IL コードは確かに 1 バイト小さくなっていますが、ほとんどの場合、呼び出しを行わないことで大きな効果が得られます (vs.2)そして、 unbox
可能な場合は完全に操作します。
// static void test1(Object o, ref int y)
// {
// int? x = o as int?;
// if (x.HasValue)
// y = x.Value;
// }
[0] valuetype [mscorlib]Nullable`1<int32> x
ldarg.0
isinst [mscorlib]Nullable`1<int32>
unbox.any [mscorlib]Nullable`1<int32>
stloc.0
ldloca.s x
call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
brfalse.s L_001e
ldarg.1
ldloca.s x
call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
stind.i4
L_001e: ret
// static void test2(Object o, ref int y)
// {
// if (o is int x)
// y = x;
// }
[0] int32 x,
[1] object obj2
ldarg.0
stloc.1
ldloc.1
isinst int32
ldnull
cgt.un
dup
brtrue.s L_0011
ldc.i4.0
br.s L_0017
L_0011: ldloc.1
unbox.any int32
L_0017: stloc.0
brfalse.s L_001d
ldarg.1
ldloc.0
stind.i4
L_001d: ret
新しい製品のパフォーマンスに関する私の発言を裏付けるさらなるテストのために C#7 以前に使用可能なオプションを超える構文については、を参照してください。 ここ (特に例「D」)。
私はそれをしようとする時間がありませんが、あなたがしたいことがあります:
foreach (object o in values)
{
int? x = o as int?;
タグなど
int? x;
foreach (object o in values)
{
x = o as int?;
あなたは完全に問題を説明しませんが、貢献するかもしれない、毎回新しいオブジェクトを作成している。
私は正確な型チェック構文を試してみました。
typeof(int) == item.GetType()
、常にitem is int
バージョンの速さで行い、そして数(重点を:あなたは配列にNullable<int>
を書いた場合でも、あなたが使用typeof(int)
する必要があります)を返します。また、ここでは、追加のnull != item
チェックが必要になります。
ただし、
typeof(int?) == item.GetType()
の滞在は、高速(item is int?
とは対照的に)が、常にfalseを返します。
typeofの構築物は、私の目には、の正確なの型チェックのための最速の方法です。この場合、正確な型がNULL可能で一致していないので、私の推測では、is/as
は、それが実際にのNullable型のインスタンスであることを確実にすることで、ここで追加のheavyliftingを行う必要がありますされます。
そして:あなたのis Nullable<xxx> plus HasValue
があなたに何を買うのか?何もありません。あなたはいつも(この場合)基礎となる(値)の型に直接行くことができます。あなたは、どちらかの値か「あなたが求めていたタイプの、いや、ないインスタンス」を取得します。あなたは配列に(int?)null
を書いた場合であっても、型チェックはfalseを返します。
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i + 1] = "";
values[i + 2] = 1;
}
FindSumWithCast(values);
FindSumWithAsAndHas(values);
FindSumWithAsAndIs(values);
FindSumWithIsThenAs(values);
FindSumWithIsThenConvert(values);
FindSumWithLinq(values);
Console.ReadLine();
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int)o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsAndHas(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As and Has: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsAndIs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (o is int)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As and Is: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithIsThenAs(object[] values)
{
// Apple-to-apple comparison with Cast routine above.
// Using the similar steps in Cast routine above,
// the AS here cannot be slower than Linq.
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int? x = o as int?;
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("Is then As: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithIsThenConvert(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = Convert.ToInt32(o);
sum += x;
}
}
sw.Stop();
Console.WriteLine("Is then Convert: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
}
を出力します:
Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811
の [EDIT:2010-06-19] の
注:前のテストは、コアi7の(会社の開発マシン)を使用して、VS2009を使用して、VS、コンフィギュレーションのデバッグ内で行われました。
以下はVS2010
を使用して、Core 2 Duoプロセッサを使用して私のマシン上で行われていましたInside VS, Configuration: Debug
Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018
Outside VS, Configuration: Debug
Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944
Inside VS, Configuration: Release
Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932
Outside VS, Configuration: Release
Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936