intel x86系列CPU既然是strong order的,不會出現loadload亂序,為什麼還需要lfence指令?

有人問看過stackoverflow了嗎?我聲明下,看過了。我把問題細化下吧。

assembly - Does it make any sense instruction LFENCE in processors x86/x86_64?

這個裡面說的答案的意思是LFENCE alone indeed seems useless for memory ordering, however it does not make SFENCE a substitute for MFENCE.但是我覺得實際使用場景中,不會把lfence和sfence結合使用。我看linux內核代碼裡面還有dpdk裡面基本沒用。實際上dpdk裡面使用lfence的地方很少,就一處。linux內核因為通用代碼都需要適配weak order的處理器,所以場景不太具有參考價值。

concurrency - Java 8 Unsafe: xxxFence() instructions

如果完全按照這個鏈接裡面說的,我覺得完全說的通,x86 has separate load-order buffers (LOBs) 。不過我在intel的手冊上沒找到哪個地方有這個說明。

Memory Barriers: a Hardware View for Software Hackers 這個文檔裡面有個圖,我覺得這裡面的Invalidate Queue可能就是上面說的那個load-order buffer。不是很確定。

Memory Barriers: a Hardware View for Software Hackers裡面還有個例子:

1 void foo(void)

2 {

3 a = 1;

4 smp_wmb();

5 b = 1;

6 }

7

8 void bar(void)

9 {

10 while (b == 0) continue;

11 smp_rmb();

12 assert(a == 1);

13 }

說明了smp_rmb的作用。在linux內核裡面smp_rmb應該就是lfence的封裝。

但是intel的手冊

明確說明了這種場景intel的處理器會有保證的,應該不需要barrier。

所以這就是我的疑問,lfence的使用場景到底有哪些?最好有實際代碼用例說明這種場景的。


關於 Streaming Loads 的部分,Bonzi Wang 已經討論過了,我來講講通常 (就是日常操作,Non-Temporal 的不算) 的情況。咱沒有Bonzi Wang那麼有能量,只能問問Intel給Xeon Phi做事的人兒。(我印象里某篇Xeon Phi的文章講到這個問題了,找不到了)

對,通常看來,它就是一條沒用的指令,但它不是 NOP. 為啥呢,因為它雖然在 load 上面沒用,在指令流上還是有點副 (?) 作用的。

lfence 它序列化的不止是讀取,而是所有的指令。lfence 之前所有的指令執行完了之後才會被執行,同時,在 lfence 完成之前其後所有的指令也不能執行 (預取是可以的)。這一點在 Intel IA32 ASDM Vol. 3A 8-17 注 4 裡面說得很清楚:

LFENCE does provide some guarantees on instruction ordering. It does not execute until all prior instructions have completed locally, and no later instruction begins execution until LFENCE completes.

舉一個例子,RDTSC 這個指令是用來讀取處理器 Time-Stamp Counter 的 (用RDMSR的話也沒人攔著你),TSC 在每個時鐘周期都會加一,通常可以用這條命令來測量某條指令到底用了幾個周期。

在亂序機器上,RDTSC 指令有可能在待測量指令執行之前就被執行了(因為 RDTSC 指令並不帶序列化功能),所以通常的寫法是

lfence
rdtsc

LFENCE 在這裡保證了 RDTSC 一定在所有待測指令都完成了之後才執行

另一條指令,RDTSCP, 有著相似的功能 (它讀的不光是 TSC,還有 Processor ID), 但是這條指令會序列化執行操作,所以不需要 lfence.

--

插一段評論討論

LFENCE是可以穿越前面的writes的。

假設一場景:

store

lfence

rtdsc

如果lfence可以穿越store,那麼就會變成

lfence

store

rtdsc

此時應該不會出現store和rtdsc的再次reorder了。 不然lfence就沒有效果了。

store 一定會在 lfence 之前執行——即,數據至少已經在store buffer裡面了,但是 —— store buffer 的數據,有可能會在 lfence 之後才 globally visible. 看 ASDM Vol. 3A 8-16 注 1:

Specifically, LFENCE does not execute until all prior instructions have completed locally, and no later instruction begins execution

until LFENCE completes. As a result, an instruction that loads from memory and that precedes an LFENCE receives data from memory

prior to completion of the LFENCE. An LFENCE that follows an instruction that stores to memory might complete before the data

being stored have become globally visible. Instructions following an LFENCE may be fetched from memory before the LFENCE, but

they will not execute until the LFENCE completes.

我把重點加了粗 尤其是那個 locally.

lfence 在這裡,確實是不保證多個執行單元之間 (globally),寫入數據的同步的 —— 即 lfence 之後,本地保證已經寫入,store 已經執行,但是全局不保證 —— 它的確只是一個 load fence.

--

至於 Streaming load/store (Non-Temporal) 的情況,lfence 確實也在用,我貼幾段 Intel 自己的某程序代碼,就不分析了。諸位自己看著琢磨琢磨吧,我實在是太懶了 (-__-)b

Snippet 1:

prefetcht0 0x200(%rsi)
prefetcht0 0x300(%rsi)
movdqu (%rsi),%xmm0
movdqu 0x10(%rsi),%xmm1
movdqu 0x20(%rsi),%xmm2
movdqu 0x30(%rsi),%xmm3
movdqu 0x40(%rsi),%xmm4
movdqu 0x50(%rsi),%xmm5
movdqu 0x60(%rsi),%xmm6
movdqu 0x70(%rsi),%xmm7
lfence
movntdq %xmm0,(%rdi)
movntdq %xmm1,0x10(%rdi)
movntdq %xmm2,0x20(%rdi)
movntdq %xmm3,0x30(%rdi)
movntdq %xmm4,0x40(%rdi)
movntdq %xmm5,0x50(%rdi)
movntdq %xmm6,0x60(%rdi)
movntdq %xmm7,0x70(%rdi)

Snippet 2 (TSC):

mov %rax,-0x8(%rbp)
xor %eax,%eax
jmp 48f480 & nopw 0x0(%rax,%rax,1)
sub $0x1,%edx
je 48f4cb & mov %ecx,%r8d
lfence
mov 0x34(%rdi),%eax
mov %eax,0x4(%rsi)
movzwl 0x32(%rdi),%eax
mov %ax,(%rsi)
mov 0x40(%rdi),%rax
mov %rax,0x8(%rsi)
movzbl 0x28(%rdi),%eax
shr $0x4,%al
and $0x1,%eax
lfence
mov 0x8(%rdi),%ecx
cmp %r8d,%ecx
jne 48f478 & and $0x1,%r8d
jne 48f478 & cmp $0x1,%al
sbb %eax,%eax
and $0xffffffffffffffa1,%eax
mov -0x8(%rbp),%rdx
xor %fs:0x28,%rdx

perf_read_tsc_conversion [ Ehh, 注意這個是內核里的,不是上面那一個程序里的 ]: (linux/tsc.c at a6a69db4b686e51045771945669bba6578af67c1 · torvalds/linux · GitHub, 4.0 RC7 的 commit), 裡面碰巧還有rmb()

int perf_read_tsc_conversion(const struct perf_event_mmap_page *pc,
struct perf_tsc_conversion *tc)
{
bool cap_user_time_zero;
u32 seq;
int i = 0;

while (1) {
seq = pc-&>lock;
rmb();
tc-&>time_mult = pc-&>time_mult;
tc-&>time_shift = pc-&>time_shift;
tc-&>time_zero = pc-&>time_zero;
cap_user_time_zero = pc-&>cap_user_time_zero;
rmb();
if (pc-&>lock == seq !(seq 1))
break;
if (++i &> 10000) {
pr_debug("failed to get perf_event_mmap_page lock
");
return -EINVAL;
}
}

if (!cap_user_time_zero)
return -EOPNOTSUPP;

return 0;
}

這貨帶注釋 (偽代碼其實是) linux/perf_event.h at a6a69db4b686e51045771945669bba6578af67c1 · torvalds/linux · GitHub :

/*
* Bits needed to read the hw events in user-space.
*
* u32 seq, time_mult, time_shift, idx, width;
* u64 count, enabled, running;
* u64 cyc, time_offset;
* s64 pmc = 0;
*
* do {
* seq = pc-&>lock;
* barrier()
*
* enabled = pc-&>time_enabled;
* running = pc-&>time_running;
*
* if (pc-&>cap_usr_time enabled != running) {
* cyc = rdtsc();
* time_offset = pc-&>time_offset;
* time_mult = pc-&>time_mu< * time_shift = pc-&>time_shift;
* }
*
* idx = pc-&>index;
* count = pc-&>offset;
* if (pc-&>cap_usr_rdpmc idx) {
* width = pc-&>pmc_width;
* pmc = rdpmc(idx - 1);
* }
*
* barrier();
* } while (pc-&>lock != seq);
*
* NOTE: for obvious reason this only works on self-monitoring
* processes.
*/


首先,澄清一下關於load-to-load order的要求,這裡面有兩個概念:

1)單核情況下,一個線程內的load-to-load order:在一個線程內沒有任何理由要求load和load之間的序;

2)多核情況下,一個線程內的load-to-load order:根據memory consistency model的不同,要求也不同。x86要求保證線程內load-to-load的order,否則就出現教課書上經典的問題(一個核先store data後store flag;另一個核先load flag後load data),這裡就不再贅述。對於更relax一點的model,比如IBM Power架構,則沒這個要求。

這裡需要清楚的是,對於load-to-load order的要求,只存在於多核情況下。這個要求的本質是,避免兩個核看到的store順序不一致,從而違背了x86的model要求。

然後,澄清一下關於load-to-load order的維護:

雖然x86的memory consistency model要求要維護load-to-load order,但實際上硬體真的是完全按順序執行的嗎?

當然不是!這樣做的效率太低了,比如第一條load是cache miss,需要訪問DRAM,而接下來的一條load沒有任何依賴,而且是cache hit,完全沒有理由要求第二條load在第一條load完成之後再執行。

再回到上面講過的load-to-load order的需求,其本質是「避免兩個核看到的store順序不一致」。也就是說只要不違背這一條,load和load之間其實是可以亂序的。

實際流水線實現中,load之間先是亂序執行,然後會有一個load-ordering-buffer的結構,在load commit之前檢測其地址是否又被其他核寫過。只有沒有衝突,這種亂序就是安全的。如果發生衝突,這種亂序就違反x86要求,需要被取消並flush流水線。

這裡實現的效果是,並不嚴格保證load-to-load order,只要亂序執行的結果和順序執行是一致的,就不違背x86 memory consistency model。換句話說,只要我一個核內的亂序沒有對整個系統的consistency造成什麼不良後果,就不會被大家追究責任。

最後,再說一下lfence的問題:

雖然,x86 memory consistency要求load-to-load order,但實際執行中,這個order並不被保證。

如果程序員確實希望保證線程內的load-to-load order,那就只能藉助lfence這種指令的。

至於真實環境下,什麼情況下用lfence,答主其實也並不太清楚,希望能有更多人補充。不過,答主在實驗室研究中倒會偶爾用到,用它保證訪存指令在memory system中執行的順序。

話說回來,lfence只不過是x86提供的一條指令,實現了一個功能,而這個功能memory consistency並沒有實現。


因為內存類型有不同,WC/WC+類型允許亂序讀。例如顯卡framebuffer。


推薦閱讀:

說說你使用Elixir/phoenix的實戰項目心得,以及對該語言/框架的前景的看法?
C++中有变量a,b,c,d,e,f作为条件表达式:如果其中任意2个及以上相同则表达式为假。怎样写?
C++高級編程、Windows程序設計和MFC,這三者學習的順序?
MFC 中 CString 與 std::string 如何相互轉換?
倘若用文言文語法編程會怎樣?

TAG:編程語言 | x86 | 多線程編程 |