簡體   English   中英

循環內的位hack與條件語句

[英]bit hack vs conditional statement inside loop

我有一個CRC計算函數,其內部循環中包含以下內容:

if (uMsgByte & 0x80) crc ^= *pChkTableOffset; pChkTableOffset++;
if (uMsgByte & 0x40) crc ^= *pChkTableOffset; pChkTableOffset++;
if (uMsgByte & 0x20) crc ^= *pChkTableOffset; pChkTableOffset++;
if (uMsgByte & 0x10) crc ^= *pChkTableOffset; pChkTableOffset++;
if (uMsgByte & 0x08) crc ^= *pChkTableOffset; pChkTableOffset++;
if (uMsgByte & 0x04) crc ^= *pChkTableOffset; pChkTableOffset++;
if (uMsgByte & 0x02) crc ^= *pChkTableOffset; pChkTableOffset++;
if (uMsgByte & 0x01) crc ^= *pChkTableOffset; pChkTableOffset++;

分析顯示,這些語句花費了大量時間。 我想知道是否可以通過將條件替換為“位hacks”來獲得一些收益。 我嘗試了以下操作,但沒有提高速度:

crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x80) - 1);
crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x40) - 1);
crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x20) - 1);
crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x10) - 1);
crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x08) - 1);
crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x04) - 1);
crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x02) - 1);
crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x01) - 1);

在最近的x86 CPU上應該更快嗎,還是有更好的方法來實現這些“位黑客”?

我不能肯定地說哪個更快,但是它們肯定是不同的-更快的速度取決於所使用的處理器品牌和型號,這在很大程度上取決於它們在[可能不可預測的]分支上的行為不同。 而且,使事情更加復雜的是,不同的處理器對於“相關計算”具有不同的行為。

我將發布的代碼轉換為以下代碼(這使生成的代碼大約長一半,但在概念上相同):

int func1(int uMsgByte, char* pChkTableOffset)
{
    int crc = 0;
    if (uMsgByte & 0x80) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x40) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x20) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x10) crc ^= *pChkTableOffset; pChkTableOffset++;

    return crc;
}


int func2(int uMsgByte, char* pChkTableOffset)
{
    int crc = 0;

    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x80) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x40) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x20) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x10) - 1);

    return crc;
}

並用clang++ -S -O2編譯:

func1:

_Z5func1jPh:                            # @_Z5func1jPh
        xorl    %eax, %eax
        testb   %dil, %dil
        jns     .LBB0_2
        movzbl  (%rsi), %eax
.LBB0_2:                                # %if.end
        testb   $64, %dil
        je      .LBB0_4
        movzbl  1(%rsi), %ecx
        xorl    %ecx, %eax
.LBB0_4:                                # %if.end.6
        testb   $32, %dil
        je      .LBB0_6
        movzbl  2(%rsi), %ecx
        xorl    %ecx, %eax
.LBB0_6:                                # %if.end.13
        testb   $16, %dil
        je      .LBB0_8
        movzbl  3(%rsi), %ecx
        xorl    %ecx, %eax
.LBB0_8:                                # %if.end.20
        retq

func2:

_Z5func2jPh:                            # @_Z5func2jPh
        movzbl  (%rsi), %eax
        movl    %edi, %ecx
        shll    $24, %ecx
        sarl    $31, %ecx
        andl    %eax, %ecx
        movzbl  1(%rsi), %eax
        movl    %edi, %edx
        shll    $25, %edx
        sarl    $31, %edx
        andl    %edx, %eax
        xorl    %ecx, %eax
        movzbl  2(%rsi), %ecx
        movl    %edi, %edx
        shll    $26, %edx
        sarl    $31, %edx
        andl    %ecx, %edx
        movzbl  3(%rsi), %ecx
        shll    $27, %edi
        sarl    $31, %edi
        andl    %ecx, %edi
        xorl    %edx, %edi
        xorl    %edi, %eax
        retq

如您所見,編譯器會為第一個版本生成分支,並在第二個版本上使用邏輯操作-每種情況下還要進行一些操作。

我可以編寫一些代碼來對每個循環進行基准測試,但是我保證結果將在不同版本的x86處理器之間有很大差異。

我不確定這是否是常見的CRC計算,但是大多數CRC計算都使用表和其他“聰明的東西”來優化版本,以比此更快的方式執行正確的計算。

有興趣看一個人是否可以擊敗一個優化的編譯器,我用兩種方式編寫了您的算法:

在這里,您表達的意圖就像是在編寫機器代碼一樣

std::uint32_t foo1(std::uint8_t uMsgByte, 
                   std::uint32_t crc, 
                   const std::uint32_t* pChkTableOffset)
{
    if (uMsgByte & 0x80) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x40) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x20) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x10) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x08) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x04) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x02) crc ^= *pChkTableOffset; pChkTableOffset++;
    if (uMsgByte & 0x01) crc ^= *pChkTableOffset; pChkTableOffset++;

    return crc;
}

在這里,我以一種更加算法化的方式表達了意圖。

std::uint32_t foo2(std::uint8_t uMsgByte, 
                   std::uint32_t crc, 
                   const std::uint32_t* pChkTableOffset)
{
    for (int i = 0 ; i < 7 ; ++i) {
        if (uMsgByte & (0x01 << (7-i)))
            crc ^= pChkTableOffset[i];

    }
    return crc;
}

然后我使用g ++ -O3進行編譯,結果是...

兩個函數中的目標代碼完全相同

故事的寓意:選擇正確的算法,避免重復,編寫優美的代碼,讓優化者來做。

這是證明:

__Z4foo1hjPKj:                          ## @_Z4foo1hjPKj
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    testb   $-128, %dil
    je  LBB0_2
## BB#1:
    xorl    (%rdx), %esi
LBB0_2:
    testb   $64, %dil
    je  LBB0_4
## BB#3:
    xorl    4(%rdx), %esi
LBB0_4:
    testb   $32, %dil
    je  LBB0_6
## BB#5:
    xorl    8(%rdx), %esi
LBB0_6:
    testb   $16, %dil
    je  LBB0_8
## BB#7:
    xorl    12(%rdx), %esi
LBB0_8:
    testb   $8, %dil
    je  LBB0_10
## BB#9:
    xorl    16(%rdx), %esi
LBB0_10:
    testb   $4, %dil
    je  LBB0_12
## BB#11:
    xorl    20(%rdx), %esi
LBB0_12:
    testb   $2, %dil
    je  LBB0_14
## BB#13:
    xorl    24(%rdx), %esi
LBB0_14:
    testb   $1, %dil
    je  LBB0_16
## BB#15:
    xorl    28(%rdx), %esi
LBB0_16:
    movl    %esi, %eax
    popq    %rbp
    retq
    .cfi_endproc

    .globl  __Z4foo2hjPKj
    .align  4, 0x90
__Z4foo2hjPKj:                          ## @_Z4foo2hjPKj
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp3:
    .cfi_def_cfa_offset 16
Ltmp4:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp5:
    .cfi_def_cfa_register %rbp
    testb   $-128, %dil
    je  LBB1_2
## BB#1:
    xorl    (%rdx), %esi
LBB1_2:
    testb   $64, %dil
    je  LBB1_4
## BB#3:
    xorl    4(%rdx), %esi
LBB1_4:
    testb   $32, %dil
    je  LBB1_6
## BB#5:
    xorl    8(%rdx), %esi
LBB1_6:
    testb   $16, %dil
    je  LBB1_8
## BB#7:
    xorl    12(%rdx), %esi
LBB1_8:
    testb   $8, %dil
    je  LBB1_10
## BB#9:
    xorl    16(%rdx), %esi
LBB1_10:
    testb   $4, %dil
    je  LBB1_12
## BB#11:
    xorl    20(%rdx), %esi
LBB1_12:
    testb   $2, %dil
    je  LBB1_14
## BB#13:
    xorl    24(%rdx), %esi
LBB1_14:
    movl    %esi, %eax
    popq    %rbp
    retq
    .cfi_endproc

有趣的是,看看編譯器在使用邏輯運算而非條件語句的代碼版本中是否也表現出色。

給出:

std::uint32_t logical1(std::uint8_t uMsgByte, 
                       std::uint32_t crc, 
                       const std::uint32_t* pChkTableOffset)
{
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x80) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x40) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x20) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x10) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x8) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x4) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x2) - 1);
    crc ^= *pChkTableOffset++ & (!(uMsgByte & 0x1) - 1);

    return crc;
}

生成的機器代碼為:

8個批次:

    movl    %edi, %eax     ; get uMsgByte into eax
    shll    $24, %eax      ; shift it left 24 bits so that bit 7 is in the sign bit
    sarl    $31, %eax      ; arithmetic shift right to copy the sign bit into all other bits
    andl    (%rdx), %eax   ; and the result with the value from the table
    xorl    %esi, %eax     ; exclusive-or into crc

因此,簡短的答案是肯定的-它執行得很好(消除了pChkTableOffset的冗余增量)

它更快嗎? 誰知道。 可能無法測量-兩種情況下的內存讀取次數相同。 編譯器可以計算出避免分支更好還是比您更好(取決於編譯器針對其優化的體系結構)。

它更優雅,更易讀嗎? 對於我自己,不。 這是我在以下情況下曾經編寫的代碼:

  • c還是一門年輕的語言
  • 處理器非常簡單,我可以更好地進行優化
  • 處理器太慢了,我不得不

這些都不適用。

如果此校驗和確實是CRC,則有一種更有效的實現方法。

假設它是CRC16:

標頭:

class CRC16
{
public:
    CRC16(const unsigned short poly);
    unsigned short CalcCRC(unsigned char * pbuf, int len);

protected:
    unsigned short CRCTab[256];
    unsigned long SwapBits(unsigned long swap, int bits);
};

實現方式:

CRC16::CRC16(const unsigned short poly)
{
    for(int i = 0; i < 256; i++) {
        CRCTab[i] = SwapBits(i, 8) << 8;
        for(int j = 0; j < 8; j++)
            CRCTab[i] = (CRCTab[i] << 1) ^ ((CRCTab[i] & 0x8000) ? poly : 0);
        CRCTab[i] = SwapBits(CRCTab[i], 16);
    }
}

unsigned long CRC16::SwapBits(unsigned long swap, int bits)
{
    unsigned long r = 0;
    for(int i = 0; i < bits; i++) {
        if(swap & 1) r |= 1 << (bits - i - 1);
        swap >>= 1;
    }
    return r;
}

unsigned short CRC16::CalcCRC(unsigned char * pbuf, int len)
{
    unsigned short r = 0;
    while(len--) r = (r >> 8) ^ CRCTab[(r & 0xFF) ^ *(pbuf++)];
    return r;
}

如您所見,消息的每個字節僅使用一次,而不是8次。

CRC8有類似的實現。

出於興趣,擴展了alain預先計算CRC表的出色建議,在我看來可以修改此類以利用c ++ 14的constexpr

#include <iostream>
#include <utility>
#include <string>

class CRC16
{
private:

    // the storage for the CRC table, to be computed at compile time
    unsigned short CRCTab[256];

    // private template-expanded constructor allows folded calls to SwapBits at compile time
    template<std::size_t...Is>
    constexpr CRC16(const unsigned short poly, std::integer_sequence<std::size_t, Is...>)
    : CRCTab { SwapBits(Is, 8) << 8 ... }
    {}

    // swap bits at compile time
    static constexpr unsigned long SwapBits(unsigned long swap, int bits)
    {
        unsigned long r = 0;
        for(int i = 0; i < bits; i++) {
            if(swap & 1) r |= 1 << (bits - i - 1);
            swap >>= 1;
        }
        return r;
    }


public:

    // public constexpr defers to private template expansion...
    constexpr CRC16(const unsigned short poly)
    : CRC16(poly, std::make_index_sequence<256>())
    {
        //... and then modifies the table - at compile time
        for(int i = 0; i < 256; i++) {
            for(int j = 0; j < 8; j++)
                CRCTab[i] = (CRCTab[i] << 1) ^ ((CRCTab[i] & 0x8000) ? poly : 0);
            CRCTab[i] = SwapBits(CRCTab[i], 16);
        }
    }

    // made const so that we can instantiate constexpr CRC16 objects
    unsigned short CalcCRC(const unsigned char * pbuf, int len) const
    {
        unsigned short r = 0;
        while(len--) r = (r >> 8) ^ CRCTab[(r & 0xFF) ^ *(pbuf++)];
        return r;
    }

};



int main()
{
    // create my constexpr CRC16 object at compile time
    constexpr CRC16 crctab(1234);

    // caclulate the CRC of something...
    using namespace std;
    auto s = "hello world"s;

    auto crc = crctab.CalcCRC(reinterpret_cast<const unsigned char*>(s.data()), s.size());

    cout << crc << endl;

    return 0;
}

然后,CRC16(1234)的構造函數可以歸結為:

__ZZ4mainE6crctab:
    .short  0                       ## 0x0
    .short  9478                    ## 0x2506
    .short  18956                   ## 0x4a0c
    .short  28426                   ## 0x6f0a
    .short  601                     ## 0x259
    .short  10079                   ## 0x275f
    .short  18517                   ## 0x4855
    .short  27987                   ## 0x6d53
... etc.

整個字符串的CRC計算如下:

        leaq    __ZZ4mainE6crctab(%rip), %rdi ; <- referencing const data :)
        movzwl  (%rdi,%rdx,2), %edx
        jmp     LBB0_8
LBB0_4:
        xorl    %edx, %edx
        jmp     LBB0_11
LBB0_6:
        xorl    %edx, %edx
LBB0_8:                                 ## %.lr.ph.i.preheader.split
        testl   %esi, %esi
        je      LBB0_11
## BB#9:
        leaq    __ZZ4mainE6crctab(%rip), %rsi
        .align  4, 0x90
LBB0_10:                                ## %.lr.ph.i
                                        ## =>This Inner Loop Header: Depth=1
        movzwl  %dx, %edi
        movzbl  %dh, %edx  # NOREX
        movzbl  %dil, %edi
        movzbl  (%rcx), %ebx
        xorq    %rdi, %rbx
        xorw    (%rsi,%rbx,2), %dx
        movzwl  %dx, %edi
        movzbl  %dh, %edx  # NOREX
        movzbl  %dil, %edi
        movzbl  1(%rcx), %ebx
        xorq    %rdi, %rbx
        xorw    (%rsi,%rbx,2), %dx
        addq    $2, %rcx
        addl    $-2, %eax
        jne     LBB0_10
LBB0_11:

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM