2015-04-02 35 views
10

當我在JavaScript中實現ChaCha20時,我偶然發現了一些奇怪的行爲。奇怪的JavaScript性能

我的第一個版本是建立這樣的(我們稱之爲「封裝版本」):

function quarterRound(x, a, b, c, d) { 
    x[a] += x[b]; x[d] = ((x[d]^x[a]) << 16) | ((x[d]^x[a]) >>> 16); 
    x[c] += x[d]; x[b] = ((x[b]^x[c]) << 12) | ((x[b]^x[c]) >>> 20); 
    x[a] += x[b]; x[d] = ((x[d]^x[a]) << 8) | ((x[d]^x[a]) >>> 24); 
    x[c] += x[d]; x[b] = ((x[b]^x[c]) << 7) | ((x[b]^x[c]) >>> 25); 
} 

function getBlock(buffer) { 
    var x = new Uint32Array(16); 

    for (var i = 16; i--;) x[i] = input[i]; 
    for (var i = 20; i > 0; i -= 2) { 
     quarterRound(x, 0, 4, 8,12); 
     quarterRound(x, 1, 5, 9,13); 
     quarterRound(x, 2, 6,10,14); 
     quarterRound(x, 3, 7,11,15); 
     quarterRound(x, 0, 5,10,15); 
     quarterRound(x, 1, 6,11,12); 
     quarterRound(x, 2, 7, 8,13); 
     quarterRound(x, 3, 4, 9,14); 
    } 
    for (i = 16; i--;) x[i] += input[i]; 
    for (i = 16; i--;) U32TO8_LE(buffer, 4 * i, x[i]); 
    input[12]++; 
    return buffer; 
} 

要(開銷參數等),減少不必要的函數調用我刪除了quarterRound - 函數,並把它的內容內聯(這是正確的,我驗證了其對一些測試向量):

function getBlock(buffer) { 
    var x = new Uint32Array(16); 

    for (var i = 16; i--;) x[i] = input[i]; 
    for (var i = 20; i > 0; i -= 2) { 
     x[ 0] += x[ 4]; x[12] = ((x[12]^x[ 0]) << 16) | ((x[12]^x[ 0]) >>> 16); 
     x[ 8] += x[12]; x[ 4] = ((x[ 4]^x[ 8]) << 12) | ((x[ 4]^x[ 8]) >>> 20); 
     x[ 0] += x[ 4]; x[12] = ((x[12]^x[ 0]) << 8) | ((x[12]^x[ 0]) >>> 24); 
     x[ 8] += x[12]; x[ 4] = ((x[ 4]^x[ 8]) << 7) | ((x[ 4]^x[ 8]) >>> 25); 
     x[ 1] += x[ 5]; x[13] = ((x[13]^x[ 1]) << 16) | ((x[13]^x[ 1]) >>> 16); 
     x[ 9] += x[13]; x[ 5] = ((x[ 5]^x[ 9]) << 12) | ((x[ 5]^x[ 9]) >>> 20); 
     x[ 1] += x[ 5]; x[13] = ((x[13]^x[ 1]) << 8) | ((x[13]^x[ 1]) >>> 24); 
     x[ 9] += x[13]; x[ 5] = ((x[ 5]^x[ 9]) << 7) | ((x[ 5]^x[ 9]) >>> 25); 
     x[ 2] += x[ 6]; x[14] = ((x[14]^x[ 2]) << 16) | ((x[14]^x[ 2]) >>> 16); 
     x[10] += x[14]; x[ 6] = ((x[ 6]^x[10]) << 12) | ((x[ 6]^x[10]) >>> 20); 
     x[ 2] += x[ 6]; x[14] = ((x[14]^x[ 2]) << 8) | ((x[14]^x[ 2]) >>> 24); 
     x[10] += x[14]; x[ 6] = ((x[ 6]^x[10]) << 7) | ((x[ 6]^x[10]) >>> 25); 
     x[ 3] += x[ 7]; x[15] = ((x[15]^x[ 3]) << 16) | ((x[15]^x[ 3]) >>> 16); 
     x[11] += x[15]; x[ 7] = ((x[ 7]^x[11]) << 12) | ((x[ 7]^x[11]) >>> 20); 
     x[ 3] += x[ 7]; x[15] = ((x[15]^x[ 3]) << 8) | ((x[15]^x[ 3]) >>> 24); 
     x[11] += x[15]; x[ 7] = ((x[ 7]^x[11]) << 7) | ((x[ 7]^x[11]) >>> 25); 
     x[ 0] += x[ 5]; x[15] = ((x[15]^x[ 0]) << 16) | ((x[15]^x[ 0]) >>> 16); 
     x[10] += x[15]; x[ 5] = ((x[ 5]^x[10]) << 12) | ((x[ 5]^x[10]) >>> 20); 
     x[ 0] += x[ 5]; x[15] = ((x[15]^x[ 0]) << 8) | ((x[15]^x[ 0]) >>> 24); 
     x[10] += x[15]; x[ 5] = ((x[ 5]^x[10]) << 7) | ((x[ 5]^x[10]) >>> 25); 
     x[ 1] += x[ 6]; x[12] = ((x[12]^x[ 1]) << 16) | ((x[12]^x[ 1]) >>> 16); 
     x[11] += x[12]; x[ 6] = ((x[ 6]^x[11]) << 12) | ((x[ 6]^x[11]) >>> 20); 
     x[ 1] += x[ 6]; x[12] = ((x[12]^x[ 1]) << 8) | ((x[12]^x[ 1]) >>> 24); 
     x[11] += x[12]; x[ 6] = ((x[ 6]^x[11]) << 7) | ((x[ 6]^x[11]) >>> 25); 
     x[ 2] += x[ 7]; x[13] = ((x[13]^x[ 2]) << 16) | ((x[13]^x[ 2]) >>> 16); 
     x[ 8] += x[13]; x[ 7] = ((x[ 7]^x[ 8]) << 12) | ((x[ 7]^x[ 8]) >>> 20); 
     x[ 2] += x[ 7]; x[13] = ((x[13]^x[ 2]) << 8) | ((x[13]^x[ 2]) >>> 24); 
     x[ 8] += x[13]; x[ 7] = ((x[ 7]^x[ 8]) << 7) | ((x[ 7]^x[ 8]) >>> 25); 
     x[ 3] += x[ 4]; x[14] = ((x[14]^x[ 3]) << 16) | ((x[14]^x[ 3]) >>> 16); 
     x[ 9] += x[14]; x[ 4] = ((x[ 4]^x[ 9]) << 12) | ((x[ 4]^x[ 9]) >>> 20); 
     x[ 3] += x[ 4]; x[14] = ((x[14]^x[ 3]) << 8) | ((x[14]^x[ 3]) >>> 24); 
     x[ 9] += x[14]; x[ 4] = ((x[ 4]^x[ 9]) << 7) | ((x[ 4]^x[ 9]) >>> 25); 
    } 
    for (i = 16; i--;) x[i] += input[i]; 
    for (i = 16; i--;) U32TO8_LE(buffer, 4 * i, x[i]); 
    input[12]++; 
    return buffer; 
} 

但業績結果如預期不大:

Encapsulated performance

Inline performance

而Firefox和Safari瀏覽器下的性能差異neglectible或不重要的Chrome下的性能切是巨大的...... 任何想法,爲什麼出現這種情況?

PS:如果圖像是小,在一個新的標籤:)

PP.S打開它們:這裏是鏈接:

Inlined

Encapsulated

+0

評論是不適合擴展討論;這個對話已經[轉移到聊天](http://chat.stackoverflow.com/rooms/74430/discussion-on-question-by-k-biermann-strange-javascript-performance)。 – 2015-04-03 14:43:53

+0

1)創建數組的成本很高:重新使用相同的緩衝區。 2)告訴我們你的U32TO8_LE,這可能是昂貴的。 3)在quarterRound中,緩存所有值,進行數學計算,然後存儲結果。這裏的高收益,我猜(8陣列間接而不是... 28!)。 4)你也可以考慮將8個函數與相關參數綁定,只將x更改爲最後一個參數而不是第一個參數。非常肯定的是,所有這些表演將會飛漲。 – GameAlchemist 2015-04-10 13:17:42

回答

21

迴歸發生是因爲你在V8的當前優化編譯器Crankshaft中的某個傳球中遇到了一個錯誤。

如果您看看曲軸對慢速「內聯」情況的作用,您會注意到getBlock函數會不斷地去最優化。

要看到您可以將--trace-deopt標誌傳遞給V8,並讀取它轉儲到控制檯的輸出或使用名爲IRHydra的工具。

我收集了兩個內聯和uninlined案件V8輸出,可以在IRHydra探索:

下面是它顯示了「內聯」情況:

method list

函數列表中的每個條目都是單個優化嘗試。紅色意味着優化的功能後來被優化,因爲優化編譯器的某些假設被違反了。

這意味着getBlock不斷優化和去最佳化。有沒有像這樣的「封裝」的情況:

enter image description here

這裏getBlock優化一次,從來沒有deoptimizes。

如果我們看一下里面getBlock我們將看到它是從Uint32Array是deoptimizes因爲這種負荷的結果是不適合int32值的值陣列負載。

enter image description here

此deopt的原因是一個有點令人費解。 JavaScript唯一的數字類型是雙精度浮點數。使用它進行所有計算會有點低效,所以優化JIT通常會嘗試將優化代碼中的整數值表示爲實際整數。

曲軸最寬的整數表示形式爲int32,其中一半的值不能在其中表示。爲了部分緩解這種限制,曲軸執行稱爲uint32分析的優化通道。此通道試圖找出將uint32值表示爲int32值是否安全 - 這是通過查看如何使用此值來完成的:某些操作(例如,並不關心「符號」,而只關心個別位,其他操作(例如去優化或從整數到雙精度的轉換)可以教導以特殊方式處理int32-that-is-actually-uint32。如果分析成功 - 所有使用的值都是安全的 - 那麼這個操作用特殊方式標記,否則(有些用途被發現是不安全的)操作沒有標記,如果它產生的值不合適到int32範圍內(任何高於0x7fffffff的值)。

在這種情況下,分析未標記x[i]作爲一種安全uint32操作 - 所以有人去優化時的x[i]結果的int32範圍之外。沒有將x[i]標記爲安全的原因是因爲它的一個用途,即內嵌者在內聯時創建的虛假指令被認爲是不安全的。這裏是一個patch for V8修復它也包含了問題的一個小例證問題:

var u32 = new Uint32Array(1); 
u32[0] = 0xFFFFFFFF; // this uint32 value doesn't fit in int32 

function tr(x) { 
    return x|0; 
    //  ^^^ - this use is uint32-safe 
} 
function ld() { 
    return tr(u32[0]); 
    // ^^^^^^^ uint32 op, will deopt if uses are not safe 
    //  | 
    //  \--- tr is inlined into ld and an hidden artificial 
    //   HArgumentObject instruction was generated that 
    //   captured values of all parameters at entry (x) 
    //   This instruction was considered uint32-unsafe 
    //   by oversight. 
} 

while (...) ld(); 

因爲曲軸自身的內襯快用完了預算就達到U32TO8_LE調用之前你沒有打在「封裝」版本的bug現場。正如你在IRHydra只能看到前三個調用quarterRound內聯:

enter image description here

你可以通過改變U32TO8_LE(buffer, 4 * i, x[i])解決此漏洞以U32TO8_LE(buffer, 4 * i, x[i]|0)這使得x[i]價值的唯一使用UINT32安全的,不會改變結果。