2016-02-02 43 views
8

考慮下面的代碼:C#編譯器是否會優化對循環內同一方法的調用?

enum Test { 
    OptionOne, 
    OptionTwo 
} 

List<string> testValues = new List<string> { ... } // A huge collection of strings 

foreach(var val in testValues) 
{ 
    if(val == Test.OptionOne.ToString()) // **Here** 
    { 
     // Do something 
    } 
} 

編譯器將優化到Test.OptionOne.ToString()來電或將它testValues集合中調用它的每個項目?

+0

它如何優化它?的原因,它會要求每個項目! – Backs

+0

@Backs'Test.OptionOne.ToString()'是一個永遠不會改變的固定字符串。它可以通過生成一個單一的常量字符串並對其進行優化,而不是在每個循環中生成一個新的字符串。 –

+2

@Backs - 這並不真正有幫助,畢竟這是海報在這裏提出質疑的確切命題。你所做的一切都是在沒有提供任何證據或理性的情況下斷言一個職位。 – antiduh

回答

8

你的問題是循環不變式分析之一 - 編譯器是否可以檢測某個表達式,該表達式不依賴於循環的狀態來進行評估,並且沒有si影響?

有充分的理由希望編譯器能夠同時滿足這兩個命題 - 編譯器可以足夠聰明地知道在枚舉上調用ToString()不會改變;並且在枚舉上調用ToString()沒有任何明顯的副作用。也許調用該函數在某種程度上比堆棧上存儲一個額外的變量更快 -

爲什麼編譯器將積極決定不弔不變有可能是原因。

問題歸結到它是否確實

我使用VS2012目標的.Net 4.6編譯下面的程序,並啓用優化編譯。看來,編譯器沒有選擇扯起不變圈外:

public static void Main() 
    { 
     for(int i = 0; i < 10; i++) 
     { 
      Console.Out.WriteLine(i); 
      Console.Out.WriteLine(Test.Option1.ToString()); 
     } 
    } 

    public enum Test 
    { 
     Option1, 
     Option2, 
     Option3 
    } 

下面是我獲得的使用ILSpy 2.3.1程序的原始IL。請注意呼叫ToString(),正好在循環中。

.method public hidebysig static 
    void Main() cil managed 
{ 
    .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = (
     01 00 00 00 
    ) 
    // Method begins at RVA 0x2050 
    // Code size 46 (0x2e) 
    .maxstack 2 
    .entrypoint 
    .locals init (
     [0] int32 i 
    ) 

    IL_0000: ldc.i4.0 
    IL_0001: stloc.0 
    IL_0002: br.s IL_0028 
    // loop start (head: IL_0028) 
     IL_0004: call class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out() 
     IL_0009: ldloc.0 
     IL_000a: callvirt instance void [mscorlib]System.IO.TextWriter::WriteLine(int32) 
     IL_000f: call class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out() 
     IL_0014: ldc.i4.0 
     IL_0015: box TestProject.Program/Test 
---> IL_001a: callvirt instance string [mscorlib]System.Object::ToString() 
     IL_001f: callvirt instance void [mscorlib]System.IO.TextWriter::WriteLine(string) 
     IL_0024: ldloc.0 
     IL_0025: ldc.i4.1 
     IL_0026: add 
     IL_0027: stloc.0 

     IL_0028: ldloc.0 
     IL_0029: ldc.i4.s 10 
     IL_002b: blt.s IL_0004 
    // end loop 

    IL_002d: ret 
} // end of method Program::Main 

我也很好奇,看運行時JITer是否會提升不變量,但它似乎也不是。我改變了代碼以下,使裝配清潔:

public static void Main() 
    { 
     TextWriter cons = Console.Out; 
     for(int i = 0; i < 10; i++) 
     { 
      cons.WriteLine(i); 
      cons.WriteLine(Test.Option1.ToString()); 
     } 
    } 

,然後用於VS的調試器來組裝,小心以確保VS允許優化JITer。它仍然沒有提示ToString()的呼叫:

  TextWriter cons = Console.Out; 
00000000 push  rdi 
00000001 push  rsi 
00000002 sub   rsp,28h 
00000006 call  0000000050D76460 
0000000b mov   rsi,rax 
      for(int i = 0; i < 10; i++) 
0000000e xor   edi,edi 
      { 
       cons.WriteLine(i); 
00000010 mov   rcx,rsi 
00000013 mov   edx,edi 
00000015 mov   rax,qword ptr [rsi] 
00000018 mov   rax,qword ptr [rax+60h] 
0000001c call  qword ptr [rax+28h] 
       cons.WriteLine(Test.Option1.ToString()); 
0000001f mov   rcx,7FE90116770h 
00000029 call  000000005F6302D0 
0000002e mov   rcx,rsi 
00000031 xor   ecx,ecx 
00000033 mov   dword ptr [rax+8],ecx 
00000036 mov   rcx,rax 
00000039 mov   rax,qword ptr [rax] 
0000003c mov   rax,qword ptr [rax+40h] 
00000040 call  qword ptr [rax]   <---- call System.Enum.ToString() 
00000042 mov   rdx,rax 
00000045 mov   rcx,rsi 
00000048 mov   rax,qword ptr [rsi] 
0000004b mov   rax,qword ptr [rax+68h] 
0000004f call  qword ptr [rax+20h] 
      for(int i = 0; i < 10; i++) 
00000052 inc   edi 
00000054 cmp   edi,0Ah 
00000057 jl   0000000000000010 
00000059 add   rsp,28h 
      } 
     } 
+0

當然這並不表明JITer是否會將其提升出 – pm100

+1

@ pm100 - 現在有更多的編輯! :) – antiduh

+0

酷 - 不錯的工作:-) – pm100

1

沒有,但可以極大地做這樣的事情減少複雜性:

using System.Linq; 

var testValues = new List<string> { ... }; // A huge collection of strings 
var testDict = testValue.ToDictionary(elem => elem, elem => true); 

var needle = Test.OptionOne.ToString(); 
if (testDict.ContainsKey(needle)) 
{ 
    // do something 
} 

字典值檢索有O(1)的複雜性。

我覺得一個替代方案是HashSet,因爲你只有鑰匙。關於從列表建設HashSet一個有趣的問題和答案可以發現here

var testHash = new HashSet<string>(testValues); 
if (testHash.Contains(needle)) 
{ 
    // do something 
} 

基於btlog的正確的觀察,示例代碼會爲重複失敗:

[編輯] 繼斯科特的評論,我會使用的Hashset(也是O(1))包括選項。它可以通過被規避:

  • 構建直接獲取數據時的HashSet的(避免列表在首位)
  • 獲得具有不同的值的第二列表通過使用Distinct

然而,第二種方法raises the complexity to O(N)

+0

也沒有'.ToHashSet()'的原因是你可以做'新的HashSet (testValues)'。 (我會用散列集合而不是字典) –

+1

這兩種情況都有一個假設,即在原始列表中沒有重複項,因爲如果情況如此,ToDictionary將拋出異常,或者重複項並不重要,因爲HashSet不能有重複的值。 – btlog

+0

建築詞典最好是O(N),因此對於一個搜索到的詞來說,複雜性不能降低。實際上它增加了,內存消耗也增加了。 –

相關問題