Почему мой вывод "= r" (var) не выбирает тот же регистр, что и ввод "a" (var)? - PullRequest
0 голосов
/ 04 октября 2019

Я учусь использовать __asm__ volatile в GCC и обнаружил проблему. Я хочу реализовать функцию, выполняющую атомарное сравнение, обмен и возврат значения, которое ранее было сохранено в месте назначения.

Почему работает "=a"(expected) выходное ограничение, но ограничение "=r"(expected) позволяет компилятору генерировать код, которыйне работает?

Случай 1.

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

uint64_t atomic_cas(uint64_t * destination, uint64_t expected, uint64_t value){
    __asm__ volatile (
        "lock cmpxchgq %3, %1":
        "=a" (expected) :
        "m" (*destination), "a" (expected), "r" (value) :
        "memory"
    );

    return expected;
}

int main(void){
    uint64_t v1 = 10;
    uint64_t result = atomic_cas(&v1, 10, 5);
    printf("%" PRIu64 "\n", result);           //prints 10, the value before, OK
    printf("%" PRIu64 "\n", v1);               //prints 5, the new value, OK
}

Работает как положено. Теперь рассмотрим следующий случай:

Случай 2.

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

uint64_t atomic_cas(uint64_t * destination, uint64_t expected, uint64_t value){
    __asm__ volatile (
        "lock cmpxchgq %3, %1":
        "=r" (expected) ://<----- I changed a with r and expected GCC understood it from the inputs 
        "m" (*destination), "a" (expected), "r" (value) :
        "memory"
    );

    return expected;
}

int main(void){
    uint64_t v1 = 10;
    uint64_t result = atomic_cas(&v1, 10, 5);
    printf("%" PRIu64 "\n", result);            //prints 5, wrong
    printf("%" PRIu64 "\n", v1);                //prints 5, the new value, OK 
}

Я проверил сгенерированную сборку и заметил следующие вещи:

I. В обоих случаях код функции одинаков и выглядит как

   0x0000555555554760 <+0>:     mov    rax,rsi
   0x0000555555554763 <+3>:     lock cmpxchg QWORD PTR [rdi],rdx
   0x0000555555554768 <+8>:     ret 

II. Проблема возникла, когда GCC вставил atomic_cas, поэтому в последнем случае правильное значение не было передано функции printf. Вот соответствующий фрагмент disas main:

0x00000000000005f6 <+38>:    lock cmpxchg QWORD PTR [rsp],rdx
0x00000000000005fc <+44>:    lea    rsi,[rip+0x1f1]        # 0x7f4
0x0000000000000603 <+51>:    mov    rdx,rax ;  <-----This instruction is absent in the Case 2.
0x0000000000000606 <+54>:    mov    edi,0x1
0x000000000000060b <+59>:    xor    eax,eax

ВОПРОС: Почему происходит замена rax (a) на произвольный регистр (r) дать неправильный результат? Я ожидал, что это сработало в обоих случаях?

UPD. Я компилирую со следующими флагами -Wl,-z,lazy -Warray-bounds -Wextra -Wall -g3 -O3

Ответы [ 2 ]

5 голосов
/ 04 октября 2019

Инструкция cmpxchg всегда помещает результат в регистр rax. Таким образом, вам нужно использовать ограничение a, чтобы сообщить GCC, что нужно переходить из этого регистра. В случае 2 вы указываете GCC использовать вместо этого произвольный регистр с помощью r, но вы ничего не помещаете в этот регистр.

Если вы хотите использовать r, вам придетсядобавьте инструкцию mov, чтобы переместить результат из rax в этот регистр (movq %%rax, %0). Вы также должны сообщить GCC, что регистр rax изменяется инструкцией, например, добавляя его в раздел «clobbers» оператора asm. В вашем случае нет причин усложнять ситуацию таким образом.

4 голосов
/ 04 октября 2019

Прежде всего, https://gcc.gnu.org/wiki/DontUseInlineAsm. Существует в основном нулевая причина для того, чтобы бросить свой собственный CAS, по сравнению с использованием bool __atomic_compare_exchange(type *ptr, type *expected, type *desired, bool weak, int success_memorder, int failure_memorder) https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html. Это работает даже с переменными, отличными от _Atomic.


"=r" сообщает gcc, что он может запрашивать вывод в любом регистре, который хочет, поэтому он может избежать необходимости mov результата там . (Как здесь, где GCC хочет вывод в RSI как аргумент для printf). И / или поэтому он может избежать разрушения ввода, который он поместил в тот же регистр. В этом весь смысл =r вместо ограничений конкретного регистра.

Если вы хотите сообщить GCC, что регистр, который он выбирает для ввода, также является регистром вывода, используйте "+r". Или в этом случае, поскольку вам нужно выбрать RAX, используйте "+a"(expected).

. Уже есть синтаксис, чтобы компилятор выбирал один и тот же регистр для 2 ограничений с отдельными переменными для ввода и вывода, в частности, для соответствия ограничениям: "=r"(outvar) : "0"(invar).

Оптимизация была бы пропущена, если бы синтаксис не позволил бы вам описать неразрушающую инструкцию, которая могла бы производить вывод в другом регистре из входных данных.


Вы можете увидеть, что GCC на самом деле выбрал, используя ограничение в комментарии.

Помните, что встроенный asm GNU C - это просто подстановка текста в ваш шаблон. Компилятор буквально не знает, что делают инструкции asm, и даже не проверяет их правильность. (Это происходит только тогда, когда ассемблер читает выходные данные компилятора.)

    ...
    asm volatile (
    "lock cmpxchgq %3, %1   # 0 out: %0  |  2 in: %2" 
    : ...
    ...

Результирующий asm очень ясно показывает проблему ( Godbolt GCC7.4 ):

        lock cmpxchgq %rsi, (%rsp)   # 0 out: %rsi  |  2 in: %rax
        leaq    .LC0(%rip), %rdi
        xorl    %eax, %eax
        call    printf@PLT

(Я использовал синтаксис AT & T, чтобы ваш cmpxchgq %reg,mem соответствовал mem,reg порядку операндов , задокументированному Intel , хотя и GAS, и встроенный ассемблер clang, похоже, принимают его и в другом порядке. Также из-за суффикса размера операнда)

GCC использует возможность запросить вывод "=r"(expected) в RSI в качестве аргумента для printf. Ваша ошибка в том, что ваш шаблон неверно полагает, что %0 расширится до rax.


Существует множество примеров отсутствия неявной связи между входом ивывод, который случается использовать тот же C var. Например, вы можете поменять местами 2 переменные Си с помощью пустого оператора asm, просто используя ограничения. Как написать короткий блок встроенной расширенной сборки GNU для замены значений двух целочисленных переменных?

...