在GNU的网站上,有一个简单的例子可以用来演示非原子访问出现的问题。这个例子包含一个小错误,他们忘记了#include <unistd.h>
:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
struct two_words { int a, b; } memory;
static struct two_words zeros = { 0, 0 }, ones = { 1, 1 };
void handler(int signum)
{
printf ("%d,%d\n", memory.a, memory.b);
alarm (1);
}
int main (void)
{
signal (SIGALRM, handler);
memory = zeros;
alarm (1);
while (1)
{
memory = zeros;
memory = ones;
}
}
这个想法是分配memory = zeros;
或memory = ones;
需要多个周期,因此中断处理程序将能够在某个时间点打印“0 1”或“1 0”。
然而,有趣的是,对于x86-64体系结构,GCC编译器生成的汇编代码如下所示。看起来赋值是通过movq指令在一个周期内完成的:
.file "interrupt_handler.c"
.text
.comm memory,8,8
.local zeros
.comm zeros,8,8
.data
.align 8
.type ones, @object
.size ones, 8
ones:
.long 1
.long 1
.section .rodata
.LC0:
.string "%d,%d\n"
.text
.globl handler
.type handler, @function
handler:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl 4+memory(%rip), %edx
movl memory(%rip), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $1, %edi
call alarm@PLT
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size handler, .-handler
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq handler(%rip), %rsi
movl $14, %edi
call signal@PLT
movq zeros(%rip), %rax
movq %rax, memory(%rip)
movl $1, %edi
call alarm@PLT
.L3:
movq zeros(%rip), %rax
movq %rax, memory(%rip)
movq ones(%rip), %rax
movq %rax, memory(%rip)
jmp .L3
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 7.3.0-16ubuntu3) 7.3.0"
.section .note.GNU-stack,"",@progbits
在一个周期内完成两个独立的作业是怎么可能的?我认为两个不同的整型数的赋值必须发生在两个不同的内存块上,但是不知何故,在这里,它们似乎被写在同一个地方。
这个例子的变化,而不是int,我会使用双精度。在这种情况下,assembly中的while循环变为:
.L3:
movq zeros(%rip), %rax
movq 8+zeros(%rip), %rdx
movq %rax, memory(%rip)
movq %rdx, 8+memory(%rip)
movq ones(%rip), %rax
movq 8+ones(%rip), %rdx
movq %rax, memory(%rip)
movq %rdx, 8+memory(%rip)
jmp .L3
1条答案
按热度按时间qhhrdooz1#
也有人指出,这是一个老例子。在那个时代,普通机器的寄存器宽度为32位或更小。如今,拥有至少64位寄存器宽度的机器是非常常见的。这意味着如果你有2个4字节的int,你总共有8个字节,这正好是CPU的指令长度。另外,您必须注意到,这两个int并没有存储在彼此完全不相关的位置。它们可以彼此相邻地存储。这意味着GCC编译器可以在一条指令中处理它们,因为这两个int加在一起可以容纳一个寄存器,因此需要一个64位操作。这是一个非强制性优化。
对32字节SIMD向量的支持在x86 CPU(AVX)中广泛存在。因此,即使使用双精度,对于某些优化设置,也可以在单个指令中优化和处理两个双精度。可以尝试Godbolt,并看到GCC和clang优化了循环内的非原子非易失性存储,除非将优化级别降低到-Og。
但是,如果您想确保无论如何优化,您仍然可以获得所描述的行为,那么请将其设置为volatile并像
struct volatile two_words { int a; _Alignas(64) int b; } memory;
一样填充它们。