【发布时间】:2016-01-21 22:06:51
【问题描述】:
我写了一个简单的ticket lock 的简单实现。锁定部分看起来像:
struct ticket {
uint16_t next_ticket;
uint16_t now_serving;
};
void lock(ticket* tkt) {
const uint16_t my_ticket =
__sync_fetch_and_add(&tkt->next_ticket, 1);
while (tkt->now_serving != my_ticket) {
_mm_pause();
__asm__ __volatile__("":::"memory");
}
}
然后我意识到,我可以使用std::atomics 来编写,而不是使用 gcc 内部函数:
struct atom_ticket {
std::atomic<uint16_t> next_ticket;
std::atomic<uint16_t> now_serving;
};
void lock(atom_ticket* tkt) {
const uint16_t my_ticket =
tkt->next_ticket.fetch_add(1, std::memory_order_relaxed);
while (tkt->now_serving.load(std::memory_order_relaxed) != my_ticket) {
_mm_pause();
}
}
这些生成几乎相同的程序集,但后者生成额外的movzwl 指令。为什么会有这个额外的mov?有没有更好、更正确的写法lock()?
使用-march=native -O3 组装输出:
0000000000000000 <lock(ticket*)>:
0: b8 01 00 00 00 mov $0x1,%eax
5: 66 f0 0f c1 07 lock xadd %ax,(%rdi)
a: 66 39 47 02 cmp %ax,0x2(%rdi)
e: 74 08 je 18 <lock(ticket*)+0x18>
10: f3 90 pause
12: 66 39 47 02 cmp %ax,0x2(%rdi)
16: 75 f8 jne 10 <lock(ticket*)+0x10>
18: f3 c3 repz retq
1a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
0000000000000020 <lock(atom_ticket*)>:
20: ba 01 00 00 00 mov $0x1,%edx
25: 66 f0 0f c1 17 lock xadd %dx,(%rdi)
2a: 48 83 c7 02 add $0x2,%rdi
2e: eb 02 jmp 32 <lock(atom_ticket*)+0x12>
30: f3 90 pause
=> 32: 0f b7 07 movzwl (%rdi),%eax <== ???
35: 66 39 c2 cmp %ax,%dx
38: 75 f6 jne 30 <lock(atom_ticket*)+0x10>
3a: f3 c3 repz retq
为什么不直接cmp (%rdi),%dx?
【问题讨论】:
-
对于那些研究这个的人,
movzwl从源读取 16 位并将其写入 32 位目标寄存器的底部,将目标中的其余位归零。 -
Could this GCC bug 报告(实际上不是错误)与它有关吗? fetch_add 在函数定义中有
volatile? -
@TonyTheLion 这里没有
volatile。 -
@Barry 我说的是
fetch_add在其函数定义中有volatile。 -
第一个实现不会错过
now_serving成员变量的原子加载吗?第一个的反汇编看起来也像一个潜在的无限循环。如果只是比较寄存器中的数据,那么来自另一个线程的更改如何传播到等待线程?
标签: c++ multithreading x86 locking