assembly GNU网站上的非原子访问示例的问题

jrcvhitl  于 2023-05-29  发布在  其他
关注(0)|答案(1)|浏览(95)

在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
qhhrdooz

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;一样填充它们。

相关问题