2011-08-24 43 views
16

不要問我如何到達那裏,但我正在玩一些掩蔽,循環展開等等。無論如何,出於興趣,我正在考慮如何實現indexof方法,並且長話短說,所有這些掩蓋等,這個幼稚的執行:爲什麼我的string.indexof(char)更快?

public static unsafe int IndexOf16(string s, int startIndex, char c) { 
      if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
      fixed (char* cs = s) { 
       for (int i = startIndex; i < s.Length; i++) { 
        if ((cs[i]) == c) return i; 
       } 
       return -1; 
      } 
     } 

比string.IndexOf(char)更快。我寫了一些簡單的測試,它似乎完全匹配輸出。 從我的機器的一些樣本輸出號(它變化到一定程度,當然,但趨勢是明確的):

short haystack 500k runs 
1741 ms for IndexOf16 
2737 ms for IndexOf32 
2963 ms for IndexOf64 
2337 ms for string.IndexOf <-- buildin 

longer haystack: 
2888 ms for IndexOf16 
3028 ms for IndexOf32 
2816 ms for IndexOf64 
3353 ms for string.IndexOf <-- buildin 

IndexOfChar被標記的extern,所以你不能反射器吧。但我認爲這應該是(本地)執行: http://www.koders.com/cpp/fidAB4768BA4DF45482A7A2AA6F39DE9C272B25B8FE.aspx?s=IndexOfChar#L1000

他們似乎使用相同的樸素的實現。

問題來到我的腦海:

1)我失去了我在執行的東西,解釋了爲什麼它的速度更快?我只能想到擴展字符的支持,但是他們的實現表明他們沒有爲此做任何特別的事情。

2)我認爲大部分低級方法最終都會在手工彙編中實現,看起來並非如此。如果是這樣,爲什麼在本地執行它,而不是像在我的示例實現中一樣在C#中實現?

(這裏完整的測試(我認爲它太長時間貼在這裏):http://paste2.org/p/1606018

(不,這不是過早的優化,它不是一個項目,我只是搞亂):-)

更新:Thnx to Oliver提示關於nullcheck和Count參數。我已經添加了這些我IndexOf16Implementation像這樣:

public static unsafe int IndexOf16(string s, int startIndex, char c, int count = -1) { 
    if (s == null) throw new ArgumentNullException("s"); 
    if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
    if (count == -1) count = s.Length - startIndex; 
    if (count < 0 || count > s.Length - startIndex) throw new ArgumentOutOfRangeException("count"); 

    int endIndex = startIndex + count; 
    fixed (char* cs = s) { 
     for (int i = startIndex; i < endIndex; i++) { 
      if ((cs[i]) == c) return i; 
     } 
     return -1; 
    } 
} 

的數字略有變化,但它仍然是相當快顯著(32/64結果略):

short haystack 500k runs 
1908 ms for IndexOf16 
2361 ms for string.IndexOf 
longer haystack: 
3061 ms for IndexOf16 
3391 ms for string.IndexOf 

UPDATE2:此版本是更快的,但(特別是對於長草堆情況下):

public static unsafe int IndexOf16(string s, int startIndex, char c, int count = -1) { 
      if (s == null) throw new ArgumentNullException("s"); 
      if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
      if (count == -1) count = s.Length - startIndex; 
      if (count < 0 || count > s.Length - startIndex) throw new ArgumentOutOfRangeException("count"); 

      int endIndex = startIndex + count; 
      fixed (char* cs = s) { 
       char* cp = cs + startIndex; 
       for (int i = startIndex; i <= endIndex; i++, cp++) { 
        if (*cp == c) return i; 
       } 
       return -1; 
      } 
     } 

更新4: 基於與LastCoder的討論,我認爲這是依賴於架構。我的Xeon W3550似乎更喜歡這個版本,而他的i7似乎更喜歡buildin版本。我的家用機器(Athlon II)似乎介於兩者之間。儘管如此,我感到驚訝。

+0

mask1應該是0xffff而不是0xff – hazzik

+0

@hazzik thnx爲提示 – chrisaut

回答

4

可能性1) 這可能不成立(爲真)在C#中,但是當我做了優化工作的x86-64彙編我很快就發現,而基準,從一個DLL調用代碼(標記爲外部)比在我的可執行文件中實現相同的確切函數要慢。最明顯的原因是分頁和內存,DLL(外部)方法遠離其他運行代碼加載到內存中,如果以前沒有訪問它,則需要分頁。基準代碼應該執行一些熱身循環的功能是基準測試,以確保它們在記憶之前首先被分頁。

可能性2) 微軟傾向於不優化字符串函數,以最充分,所以出優化本地字符串長度,子串,的indexOf等是不是真的前所未聞的。軼事;在x86-64彙編程序中,我能夠創建WinXP64的RtlInitUnicodeString函數,幾乎在所有實際的用例中運行速度提高了2倍。

可能性3)您的基準測試代碼顯示您爲IndexOf使用2參數重載,此函數可能會調用3參數重載IndexOf(Char,Int32,Int32),這會爲每次迭代增加額外的開銷。


這可能會更快,因爲您可以刪除每次迭代的i變量增量。

  char* cp = cs + startIndex; 
      char* cpEnd = cp + endIndex; 
      while (cp <= cpEnd) { 
       if (*cp == c) return cp - cs; 
       cp++; 
      } 

編輯在回答關於(2)你的好奇心,編碼早在2005年,並用來修補我的WinXP64機ntdll.dll的。 http://board.flatassembler.net/topic.php?t=4467

RtlInitUnicodeString_Opt: ;;rcx=buff rdx=ucharstr 77bytes 
      xor r9d,r9d 
      test rdx,rdx 
      mov dword[rcx],r9d 
      mov [rcx+8],rdx 
      jz  .end 
      mov r8,rdx 
    .scan: 
      mov eax,dword[rdx] 

      test ax,ax 
      jz  .one 
      add rdx,4 
      shr eax,16 
      test ax,ax 
      jz  .two 
      jmp .scan 
    .two: 
      add rdx,2 
    .one: 
      mov eax,0fffch 
      sub rdx,r8 
      cmp rdx,0fffeh 
      cmovnb rdx,rax 
      mov [ecx],dx 
      add dx,2 
      mov [ecx+2],dx 
      ret 
    .end: 
      retn 

編輯2運行您的示例代碼(您最快的版本更新)的string.IndexOf運行速度更快的我的英特爾i7處理器,4GB內存,Win7的64位。

short haystack 500k runs 
2590 ms for IndexOf16 
2287 ms for string.IndexOf 
longer haystack: 
3549 ms for IndexOf16 
2757 ms for string.IndexOf 

優化有時非常依賴架構。

+0

Thnx。 1)代碼在基準測試時應該完全(jitted,我猜是分頁),因爲它首先測試了正確性。但好點。 2)我想看到的代碼:-) – chrisaut

+0

3)有助於inbuild一個略有下降,但還不夠: 短草垛500K運行 1669毫秒IndexOf16 2319毫秒string.IndexOf 2 2094毫秒string.IndexOf with 3 更長的乾草堆: 2289 ms for IndexOf16 3238 ms for string.IndexOf with 2 3153 ms for string.IndexOf with 3 – chrisaut

+0

關於您的版本,我幾乎完全一樣。 :-)儘管如此,仍然嘗試過你(你忘了一個演員,並返回-1)。它比較慢:〜2150 /〜2700短/長測試 不錯的建議雖然,thnx – chrisaut

2

如果你真的做了這樣的微觀測量檢查每一個位數。在MS實現中(如您在提供的鏈接中看到的),他們還檢查s是否爲null並拋出NullArgumentException。這也是包含count參數的實現。所以他們另外檢查是否計數爲正確的值並拋出ArgumentOutOfRangeException。

我認爲這些小小的檢查讓代碼更健壯,足以讓它們在如此短的時間內如此頻繁地調用它們的速度變慢一點點。

+0

Thnx爲提示和好點。我添加了字符串空檢查和計數實現。它不會顯着改變數字。看到我更新的問題。 – chrisaut

1

這可能與「固定」語句有關,因爲「它將src和dst對象的位置固定在內存中,以便它們不會被垃圾回收移動。」也許加快了方法?

此外,「不安全的代碼通過消除數組邊界檢查來提高性能」。這也可能是爲什麼。從MSDN採取

上述評論

+0

如果有任何問題需要解決,那麼與其本地實施相比,它們可能會變得更慢,因爲它們可能有其他方式與gc通信。當然,因爲它們是本地的,所以邊界檢查不應該起作用。 – chrisaut

+0

@Steven,難道答案不是「與使用動態數組分配不同,固定長度的數組將被視爲結構的一部分而不僅僅是引用,它還具有作爲非託管類型的額外好處,因此一個結構使用它將在默認情況下分配堆棧「。因此,因爲非託管代碼在堆棧上運行速度更快,那麼可能會在堆上運行一些託管代碼? – Jethro

+0

我不太清楚你的意思。我沒有分配任何數組,我嗎?你是什​​麼意思「在堆棧上運行」。 AFAIK堆棧vs堆只在mem分配中起作用。我假設這兩個實現都沒有做任何堆分配。我錯過了什麼嗎? – chrisaut

相關問題