2017-09-15 370 views
1

我試圖同時處理MSVC和GCC編譯器,同時更新這個代碼庫對GCC工作。但我不確定GCC內聯ASM的工作原理。現在我不擅長將ASM轉換爲C,否則我只會使用C而不是ASM。MSVC內聯彙編GCC的

SLONG Div16(signed long a, signed long b) 
{ 
    signed long v; 
#ifdef __GNUC__ // GCC doesnt work. 
__asm() { 
#else // MSVC 
__asm { 
#endif 
     mov edx, a 
     mov ebx, b   
     mov eax, edx   
     shl eax, 16   
     sar edx, 16    
     idiv ebx    
     mov v, eax    
    } 
    return v; 
} 

signed long ROR13(signed long val) 
{ 
    _asm{ 
     ror val, 13 
    } 
} 

我認爲ROR13工作方式類似(val << 13) | (val >> (32 - 13))但這些代碼不會產生相同的輸出。

什麼是將這種內嵌ASM到GCC的正確方法和/或最新這段代碼的C譯法?

+1

「asm」指令的不同語法不是你唯一的問題。即使對於彙編程序指令,GCC也使用[不同語法](https://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax)。 –

+0

'ror' rotate ** right ** so'(val >> 13)| (val <<(32 - 13))' – Jester

+0

[編譯器內部函數](https://en.wikipedia.org/wiki/Intrinsic_function)可能對您有所幫助。例如,這個Visual Studio [x86 Intrinsics List](https://msdn.microsoft.com/en-us/library/hh977023.aspx) –

回答

3

GCC uses a completely different syntax for inline assembly比MSVC做,所以這是相當多的工作來維持兩種形式。這也不是一個特別好的主意。 There are many problems with inline assembly。人們經常使用它,因爲他們認爲它會讓代碼運行得更快,但通常會產生相反的效果。 Unless you're an expert in both assembly language and the compiler's code-generation strategies, you are far better off letting the compiler's optimizer generate the code

當你試圖做到這一點時,你必須在這裏小心一點,但是:簽名的右移是用C實現定義的,所以如果你關心可移植性,你需要將值轉換爲等價的無符號類型:

#include <limits.h> // for CHAR_BIT 

signed long ROR13(signed long val) 
{ 
    return ((unsigned long)val >> 13) | 
      ((unsigned long)val << ((sizeof(val) * CHAR_BIT) - 13)); 
} 

(另請參閱Best practices for circular shift (rotate) operations in C++)。

這將具有相同的語義原密碼:ROR val, 13。事實上,MSVC會像GCC一樣精確地生成目標代碼。 (Clang,有趣的是,將會做ROL val, 19,由於旋轉的方式產生相同的結果,ICC 17產生了一個擴展的移位:SHLD val, val, 19。我不確定爲什麼;也許這比某些Intel處理器上的旋轉要快,或者。也許是英特爾相同的,但對AMD慢)

要在純C實現Div16,你想:

signed long Div16(signed long a, signed long b) 
{ 
    return ((long long)a << 16)/b; 
} 

在64位架構,可以做原生64位除法,(假設long仍然是一個32位的類型等在Windows上),這將轉化爲:

movsxd rax, a # sign-extend from 32 to 64, if long wasn't already 64-bit 
shl  rax, 16 
cqo    # sign-extend rax into rdx:rax 
movsxd rcx, b 
idiv rcx  # or idiv b if the inputs were already 64-bit 
ret 

不幸的是,在32位x86,代碼是不是幾乎一樣好。編譯器向內部庫函數發出調用,提供擴展的64位除法,因爲它們不能證明使用單個64b/32b => 32b idiv instruction不會發生故障。 (這將引發#DE異常,如果該商數不eax適合,而不是僅僅截斷)

換句話說,轉化:

int32_t Divide(int64_t a, int32_t b) 
{ 
    return (a/b); 
} 

到:

mov eax, a_low 
mov edx, a_high 
idiv b     # will fault if a/b is outside [-2^32, 2^32-1] 
ret 

不一個合法的優化 - 編譯器無法發出此代碼。該語言標準說64/32分區被提升爲64/64分區,總是產生64位結果。您稍後將64位結果強制轉換或強制爲32位值與分割操作本身的語義無關。對於ab的某些組合的錯誤將違反假定規則,除非編譯器可以證明ab的這些組合是不可能的。(例如,如果知道b大於1<<16,這可能是a = (int32_t)input; a <<= 16;的合法優化但儘管這會產生與所有輸入的C抽象機器相同的行爲,但gcc和clang 目前並不這樣做優化。)


目前根本沒有覆蓋語言標準規定的規則,迫使編譯器生成所需的對象代碼的好方法。 MSVC沒有爲它提供內在的功能(雖然有Windows API函數,MulDiv,它不是快速的,並且只是使用內聯彙編實現它自己的實現—和a bug in a certain case,現在由於需要向後兼容性而被強化)。實質上,您只能通過內聯或從外部模塊鏈接到組件來進行組裝。

因此,你陷入了醜陋之中。它看起來像這樣:

signed long Div16(signed long a, signed long b) 
{ 
#ifdef __GNUC__  // A GNU-style compiler (e.g., GCC, Clang, etc.) 
    signed long quotient; 
    signed long remainder; // (unused, but necessary to signal clobbering) 
    __asm__("idivl %[divisor]" 
      :   "=a" (quotient), 
         "=d" (remainder) 
      :   "0" ((unsigned long)a << 16), 
         "1" (a >> 16), 
      [divisor] "rm" (b) 
      : 
      ); 
    return quotient; 
#elif _MSC_VER  // A Microsoft-style compiler (i.e., MSVC) 
    __asm 
    { 
     mov eax, DWORD PTR [a] 
     mov edx, eax 
     shl eax, 16 
     sar edx, 16 
     idiv DWORD PTR [b] 
     // leave result in EAX, where it will be returned 
    } 
#else 
    #error "Unsupported compiler" 
#endif 
} 

這會導致在Microsoft和GNU風格的編譯器上都需要輸出。

嗯,主要是。出於某種原因,當您使用rm約束條件時,編譯器可以自由選擇將除數作爲內存操作數處理還是將其加載到寄存器中,但與僅使用r(其中的force它將其加載到一個寄存器中)。這不影響GCC或ICC。如果您關心Clang的輸出質量,那麼您可能只想使用r,因爲這會在所有編譯器上提供同樣好的目標代碼。

Live Demo on Godbolt Compiler Explorer

(注:GCC使用SAL記憶在它的輸出,而不是SHL助記符這些相同指令,區別僅事項右移和所有健全的組裝程序員使用SHL。我不知道爲什麼海灣合作委員會發出SAL,但你可以把它精神上轉換成SHL。)

+0

爲什麼ICC輪換SHLD:憤世嫉俗的原因是它在AMD上的速度較慢(6 uops,3c延遲,在Bulldozer/Ryzen上),但與Nehalem之後的Intel的ROR成本相同。不那麼憤世嫉俗的是,它並沒有太多的認識旋轉習慣用法,而是在移位計數正確互補的情況下,用SHLD實現移位和和運算。 –

+0

雖然可以識別旋轉並使用BMI2 RORX。 https://godbolt.org/g/GL5YQw。 (另見https://stackoverflow.com/questions/776508/best-practices-for-circular-shift-rotate-operations-in-c爲安全無UB變量計數旋轉,這也使得你真的點想要命名你的函數'ror32',而不是通過'long'的寬度來旋轉,如果你想要創建一個便攜函數!) –

+0

你的64位原生分割例子很奇怪/錯誤。C編譯器將使用64位操作數大小來處理'idiv'以及這些轉換。所以它會將'movsx rcx,b' /'movsx rax,a' /'shl rax,16' /'cqo'(不是cdq)/'idiv rcx'(https://godbolt.org/g/9Gf9Z3),因爲輸入還沒有被擴展到64位(假設你的輸入是'int32_t',比如Windows上的'long'或者一般的32位)。在複製到已知寄存器後顯示'shl a,16'而不是移動是奇怪的。 –