2011-04-18 14 views
13

我遇到一個非常有趣的情況,比較一個泛型方法內可空類型爲null比比較值類型或引用類型慢234倍。代碼如下:爲什麼在沒有約束的泛型方法上比較可空值類型爲null會慢些?

static bool IsNull<T>(T instance) 
{ 
    return instance == null; 
} 

的執行代碼是:

int? a = 0; 
string b = "A"; 
int c = 0; 

var watch = Stopwatch.StartNew(); 

for (int i = 0; i < 1000000; i++) 
{ 
    var r1 = IsNull(a); 
} 

Console.WriteLine(watch.Elapsed.ToString()); 

watch.Restart(); 

for (int i = 0; i < 1000000; i++) 
{ 
    var r2 = IsNull(b); 
} 

Console.WriteLine(watch.Elapsed.ToString()); 

watch.Restart(); 

for (int i = 0; i < 1000000; i++) 
{ 
    var r3 = IsNull(c); 
} 

watch.Stop(); 

Console.WriteLine(watch.Elapsed.ToString()); 
Console.ReadKey(); 

對於上述代碼的輸出是:

00:00:00.1879827

00:00: 00.0008779

00:00:00.0008532

正如你所看到的,比較一個可爲null的int與null比比較一個int或一個字符串慢234倍。如果我添加了第二個過載與正確的約束,結果發生顯着變化:

static bool IsNull<T>(T? instance) where T : struct 
{ 
    return instance == null; 
} 

現在的結果是:

00:00:00.0006040

00:00:00.0006017

00:00:00.0006014

這是爲什麼?我沒有檢查字節碼,因爲我不太流利,但即使字節碼有點不同,我期望JIT優化這個,而不是(我正在運行優化) 。

+0

有了這樣的結果 - 在最差的情況下進行1M次迭代的時間低於0.2秒,是否重要? – Oded 2011-04-18 19:30:36

+1

是的,如果你每秒鐘做這個1M的話。我做。 – 2011-04-18 19:43:02

+2

不要忘記,您正在衡量百萬次迭代*的成本和第一次調用*上代碼的成本之和。如果代碼真的很便宜,就像這段代碼一樣,只發生一次的jit成本實際上可以支配平均值。在同一個程序中運行兩次測試可能會很有趣,所以第二次,代碼是「熱門」。 – 2011-04-18 20:05:13

回答

5

如果你比較由兩個重載產生IL,你可以看到這裏是拳擊涉及:

第一個模樣:

.method private hidebysig static bool IsNull<T>(!!T instance) cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] bool CS$1$0000) 
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: box !!T 
    L_0007: ldnull 
    L_0008: ceq 
    L_000a: stloc.0 
    L_000b: br.s L_000d 
    L_000d: ldloc.0 
    L_000e: ret 
} 

雖然第二模樣:

.method private hidebysig static bool IsNull<valuetype ([mscorlib]System.ValueType) .ctor T>(valuetype [mscorlib]System.Nullable`1<!!T> instance) cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] bool CS$1$0000) 
    L_0000: nop 
    L_0001: ldarga.s instance 
    L_0003: call instance bool [mscorlib]System.Nullable`1<!!T>::get_HasValue() 
    L_0008: ldc.i4.0 
    L_0009: ceq 
    L_000b: stloc.0 
    L_000c: br.s L_000e 
    L_000e: ldloc.0 
    L_000f: ret 
} 

在第二種情況下,編譯器知道該類型是Nullable,因此它可以對此進行優化。在第一種情況下,它必須處理任何類型的引用和值類型。所以它必須跳過一些額外的籃球。至於爲什麼int比int更快,我想可能有一些JIT優化涉及到了。

3

拳擊和拆箱正在那裏發生,沒有你知道它,拳擊操作是緩慢的。這是因爲您在後臺將可空引用類型轉換爲值類型。

+1

我知道這一點。但是,那麼將int與null進行比較的性能應該等於將可爲空的與null進行比較? – 2011-04-18 19:34:42

+0

它是,或者至少應該是。但你的基準包括拳擊操作。試試另一個比較類型爲int的變量的循環嗎?如果沒有方法1,000,000次,則爲null *,然後將其與null相比較。無論如何,我不知道是否會執行裝箱(因爲'int'類型可能必須首先被轉換爲'object')。 – Ryan 2011-04-18 19:37:46

+1

@Diego:將int與null進行比較時,我們直接處理轉換運算符。比較調用一個'Equals(int?,int?)'方法,其中'int'和'null'值被隱式轉換;在這個電話中沒有拳擊。 (見http://blogs.msdn.com/b/kirillosenkov/archive/2008/09/08/why-a-comparison-of-a-value-type-with-null-is-a-warning.aspx) – 2011-04-18 19:39:04

14

以下是您應該做的調查。

首先重寫程序,使其確實全部兩次。在兩次迭代之間放置一個消息框。編譯優化程序,並運行程序而不是在調試器中。這可確保抖動生成最佳代碼。抖動知道調試器何時連接,並且可以生成更糟的代碼,以便在調試時更易於調試,如果它認爲這就是您正在做的事情。

當彈出消息框時,附加調試器,然後在彙編代碼級跟蹤代碼的三個不同版本,如果實際上甚至有三個不同的版本。我願意下注至多1美元,因爲抖動知道整個事情可以優化爲「返回false」,然後返回false可以被內聯,甚至可能會刪除循環。

(在未來,你應該寫性能測試時考慮這一點。記住,如果你不這樣做使用結果然後抖動是免費的,完全優化掉一切產生這種結果,只要因爲它沒有副作用。)

一旦你可以看看彙編代碼,你會看到發生了什麼。

我沒有這個親自調查自己,但賠率是好的,這是怎麼回事是這樣的:

  • 在INT代碼路徑

    ,抖動意識到盒裝int是永遠不能爲null並轉動方法轉換爲「return false」

  • 在字符串代碼路徑中,抖動意識到測試一個字符串爲nullity相當於測試指向該字符串的託管指針是否爲零,因此它正在生成一條測試是否寄存器爲零。

  • in int?代碼路徑,可能是抖動意識到測試一個int?無效可以通過裝箱int來實現嗎? - 由於裝箱的null int是一個空引用,因此可以簡化爲先前針對零測試託管指針的問題。但你承擔了拳擊的成本。

如果是這種情況,那麼抖動可能會更復雜在這裏,並認識到測試int?對於null可以通過返回int內的HasValue布爾值的反轉來完成。

但正如我所說,這只是一個猜測。自己生成代碼,如果感興趣,看看它在做什麼。

相關問題