簡體   English   中英

了解易失性asm與易失性變量

[英]Understanding volatile asm vs volatile variable

我們考慮以下程序,這只是定時循環:

#include <cstdlib>

std::size_t count(std::size_t n)
{
#ifdef VOLATILEVAR
    volatile std::size_t i = 0;
#else
    std::size_t i = 0;
#endif
    while (i < n) {
#ifdef VOLATILEASM
        asm volatile("": : :"memory");
#endif
        ++i;
    }
    return i;
}

int main(int argc, char* argv[])
{
    return count(argc > 1 ? std::atoll(argv[1]) : 1);
}

為了便於閱讀,具有volatile變量和volatile asm的版本如下:

#include <cstdlib>

std::size_t count(std::size_t n)
{
    volatile std::size_t i = 0;
    while (i < n) {
        asm volatile("": : :"memory");
        ++i;
    }
    return i;
}

int main(int argc, char* argv[])
{
    return count(argc > 1 ? std::atoll(argv[1]) : 1);
}

使用g++ -Wall -Wextra -g -std=c++11 -O3 loop.cpp -o loopg++ 8下進行編譯的時間大致如下:

  • default: 0m0.001s
  • -DVOLATILEASM: 0m1.171s
  • -DVOLATILEVAR: 0m5.954s
  • -DVOLATILEVAR -DVOLATILEASM: 0m5.965s

我的問題是:為什么呢? 默認版本是正常的,因為編譯器已對循環進行了優化。 但是我很難理解為什么-DVOLATILEVAR-DVOLATILEASM更長,因為兩者都應強制循環運行。

編譯器資源管理器-DVOLATILEASM提供以下count功能:

count(unsigned long):
  mov rax, rdi
  test rdi, rdi
  je .L2
  xor edx, edx
.L3:
  add rdx, 1
  cmp rax, rdx
  jne .L3
.L2:
  ret

對於-DVOLATILEVAR (以及組合的-DVOLATILEASM -DVOLATILEVAR ):

count(unsigned long):
  mov QWORD PTR [rsp-8], 0
  mov rax, QWORD PTR [rsp-8]
  cmp rdi, rax
  jbe .L2
.L3:
  mov rax, QWORD PTR [rsp-8]
  add rax, 1
  mov QWORD PTR [rsp-8], rax
  mov rax, QWORD PTR [rsp-8]
  cmp rax, rdi
  jb .L3
.L2:
  mov rax, QWORD PTR [rsp-8]
  ret

為什么會這樣呢? 為什么變量的volatile限定條件會阻止編譯器執行與asm volatile相同的循環?

當您使i volatile您告訴編譯器它不知道的某些內容可以更改其值。 這意味着每次使用它時都必須加載它的值,並且每次寫入它時都必須存儲它。 i volatile ,編譯器可以優化該同步。

-DVOLATILEVAR強制編譯器將循環計數器保留在內存中,因此循環瓶頸會導致存儲/重新加載(存儲轉發)的延遲, -DVOLATILEVAR個周期+ add 1個周期的延遲。

每次對volatile int i賦值和從volatile int i讀取的賦值都被認為是優化程序必須在內存中發生的可觀察到的副作用,而不僅僅是寄存器。 這就是volatile意思。

還需要重新加載以進行比較,但這只是吞吐量問題,而不是延遲問題。 〜6個循環循環帶有數據依賴性,這意味着您的CPU不受任何吞吐量限制的瓶頸。

這與您從-O0編譯器輸出中獲得的結果相似,因此請看一下我的回答: 添加編譯時的冗余分配可加快代碼的速度,而無需對諸如此類的更多循環以及x86存儲轉發進行優化。


僅使用VOLATILEASM ,空的asm模板( "" )必須運行正確的次數。 為空時,它不會向循環添加任何指令,因此您剩下一個2 uop add / cmp + jne循環,該循環可以在現代x86 CPU上以每個時鍾1次迭代的速度運行。

至關重要的是,盡管存在編譯器內存障礙,循環計數器仍可以保留在寄存器中。 "memory"破壞器被視為對非內聯函數的調用 :它可以讀取或修改它可能引用的任何對象,但不包括從未使用其地址轉義過該函數的局部變量。 (即我們從未調用過sscanf("0", "%d", &i)posix_memalign(&i, 64, 1234) 。但是,如果這樣做了,那么"memory"屏障將不得不溢出/重新加載它,因為外部函數可以保存指向該對象的指針。

即, "memory"破壞對象只是對可能在當前函數外部可見的對象的完整編譯器屏障。 這實際上只是一個問題,當您四處查看編譯器的輸出以查看哪些障礙可以做什么時,因為障礙僅對其他線程可能指向的變量的多線程正確性很重要。

順便說一句,您的asm語句已經隱式volatile因為它沒有輸出操作數。 (請參閱gcc手冊中的Extended-Asm#Volatile )。

您可以添加虛擬輸出以使編譯器可以優化其非易失性asm語句,但不幸的是, gcc在從中刪除了非易失性asm語句后仍保持空循環。 如果i的地址轉義了該函數,則刪除asm語句會完全在函數返回之前將循環變成對存儲的單個比較跳轉。 我認為直接返回而不存儲到該本地是合法的,因為沒有正確的程序可以知道它在i超出范圍之前設法從另一個線程讀取了i

但是無論如何,這是我使用的來源。 正如我說的,請注意,這里總是有一個asm語句,並且我正在控制它是否volatile

#include <stdlib.h>
#include <stdio.h>

#ifndef VOLATILEVAR   // compile with -DVOLATILEVAR=volatile  to apply that
#define VOLATILEVAR
#endif

#ifndef VOLATILEASM  // Different from your def; yours drops the whole asm statement
#define VOLATILEASM
#endif

// note I ported this to also be valid C, but I didn't try -xc to compile as C.
size_t count(size_t n)
{
    int dummy;  // asm with no outputs is implicitly volatile
    VOLATILEVAR size_t i = 0;
    sscanf("0", "%zd", &i);
    while (i < n) {
        asm  VOLATILEASM ("nop # operand = %0": "=r"(dummy) : :"memory");
        ++i;
    }
    return i;
}

編譯(使用gcc4.9和更高版本的-O3,均未啟用VOLATILE)到該奇怪的asm。 帶有gcc和clang的Godbolt編譯器資源管理器 ):

 # gcc8.1 -O3   with sscanf(.., &i) but non-volatile asm
 # the asm nop doesn't appear anywhere, but gcc is making clunky code.
.L8:
    mov     rdx, rax  # i, <retval>
.L3:                                        # first iter entry point
    lea     rax, [rdx+1]      # <retval>,
    cmp     rax, rbx  # <retval>, n
    jb      .L8 #,

干得好,GCC .... gcc4.8 -O3避免拉一個額外的mov內循環:

 # gcc4.8 -O3   with sscanf(.., &i) but non-volatile asm
.L3:
    add     rdx, 1    # i,
    cmp     rbx, rdx  # n, i
    ja      .L3 #,

    mov     rax, rdx  # i.0, i   # outside the loop

無論如何,如果沒有偽輸出操作數或帶有volatile ,gcc8.1會給我們:

 # gcc8.1  with sscanf(&i) and asm volatile("nop" ::: "memory")
.L3:
    nop # operand = eax     # dummy
    mov     rax, QWORD PTR [rsp+8]    # tmp96, i
    add     rax, 1    # <retval>,
    mov     QWORD PTR [rsp+8], rax    # i, <retval>
    cmp     rax, rbx  # <retval>, n
    jb      .L3 #,

因此,我們看到了循環計數器的相同存儲/重載,只是與volatile i區別( volatile icmp不需要重載)。

我使用nop而不是僅添加注釋,因為Godbolt默認情況下隱藏僅注釋行,我希望看到它。 對於gcc,它純粹是文本替換:我們正在查看編譯器的asm輸出,其中將操作數替換為模板,然后將其發送到匯編器。 對於clang來說,可能會有一些效果,因為asm必須有效(即實際上正確地組裝了)。

如果我們注釋掉scanf並刪除偽輸出操作數,則會得到其中只有nop的僅寄存器循環。 但是請保留偽輸出操作數,並且nop不會出現在任何地方。

暫無
暫無

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

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