понимание того, как работает «изменчивое» ключевое слово и сравнение - PullRequest
0 голосов
/ 13 марта 2019

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

int main() {
    /* volatile */ int lock = 999;
    while (lock);
}

На компиляторе x86-64-clang-3.0.0 , его код сборки следующий.

main:                                   # @main
        mov     DWORD PTR [RSP - 4], 0
        mov     DWORD PTR [RSP - 8], 999


.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        cmp     DWORD PTR [RSP - 8], 0
        je      .LBB0_3
        jmp     .LBB0_1


.LBB0_3:
        mov     EAX, DWORD PTR [RSP - 4]
        ret

Когда ключевое слово volatile комментируется, получается следующее.

main:                                   # @main
        mov     DWORD PTR [RSP - 4], 0
        mov     DWORD PTR [RSP - 8], 999


.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        mov     EAX, DWORD PTR [RSP - 8]
        cmp     EAX, 0
        je      .LBB0_3
        jmp     .LBB0_1


.LBB0_3:
        mov     EAX, DWORD PTR [RSP - 4]
        ret

Вопросы, которые мне интересны и не понятны,

  • cmp DWORD PTR [RSP - 8], 0. <--- Почему сравнение выполняется с <code>0, тогда как DWORD PTR [RSP - 8] содержит 999 в пределах?
  • Почему DWORD PTR [RSP - 8] копируется в EAX и снова, почему выполняется сравнение между 0 и EAX?

Ответы [ 2 ]

5 голосов
/ 13 марта 2019

Похоже, вы забыли включить оптимизацию. -O0 обрабатывает все переменные (кроме register переменных) почти так же, как volatile для согласованной отладки .

При включенной оптимизации компиляторы могут выводить энергонезависимые нагрузки из циклов. while(locked); скомпилируется аналогично источнику, как

if (locked) {
    while(1){}
}

Или, поскольку locked имеет инициализатор с постоянной времени компиляции, вся функция должна компилироваться в jmp main (бесконечный цикл).

См. Программирование MCU - оптимизация C ++ O2 обрывается во время цикла для получения дополнительной информации.


Почему DWORD PTR [RSP - 8] копируется в EAX и опять же, почему выполняется сравнение между 0 и EAX?

Некоторые компиляторы хуже складывают загрузки в операнды памяти для других инструкций при использовании volatile. Я думаю, именно поэтому вы получаете отдельную загрузку mov здесь; это просто пропущенная оптимизация.

(Хотя cmp [mem], imm может быть менее эффективным. Я забыл, может ли он слиться макросом с JCC или чем-то подобным. В режиме адресации, относящейся к RIP, он не смог бы слиться с нагрузкой, но база регистров в порядке.)


cmp EAX, 0 странно, я полагаю, что clang с отключенной оптимизацией не ищет test eax,eax в качестве оптимизации глазка для сравнения с нулем.

Как прокомментировал @ user3386109, locked в логическом контексте эквивалентно locked != 0 в C / C ++.

2 голосов
/ 13 марта 2019

Компилятор не знает о кешировании, это не кеширование, это говорит компилятору, что значение может меняться между доступами. Таким образом, чтобы функционально реализовать наш код, он должен выполнять обращения, которые мы запрашиваем, в том порядке, в котором мы их запрашиваем. Не могу оптимизировать.

void fun1 ( void )
{
    /* volatile */ int lock = 999;
    while (lock) continue;
}
void fun2 ( void )
{
    volatile int lock = 999;
    while (lock) continue;
}
volatile int vlock;
int ulock;
void fun3 ( void )
{
    while(vlock) continue;
}
void fun4 ( void )
{
    while(ulock) continue;
}
void fun5 ( void )
{
    vlock=3;
    vlock=4;
}
void fun6 ( void )
{
    ulock=3;
    ulock=4;
}

Мне легче видеть на руке ... не имеет значения.

Disassembly of section .text:

00001000 <fun1>:
    1000:   eafffffe    b   1000 <fun1>

00001004 <fun2>:
    1004:   e59f3018    ldr r3, [pc, #24]   ; 1024 <fun2+0x20>
    1008:   e24dd008    sub sp, sp, #8
    100c:   e58d3004    str r3, [sp, #4]
    1010:   e59d3004    ldr r3, [sp, #4]
    1014:   e3530000    cmp r3, #0
    1018:   1afffffc    bne 1010 <fun2+0xc>
    101c:   e28dd008    add sp, sp, #8
    1020:   e12fff1e    bx  lr
    1024:   000003e7    andeq   r0, r0, r7, ror #7

00001028 <fun3>:
    1028:   e59f200c    ldr r2, [pc, #12]   ; 103c <fun3+0x14>
    102c:   e5923000    ldr r3, [r2]
    1030:   e3530000    cmp r3, #0
    1034:   012fff1e    bxeq    lr
    1038:   eafffffb    b   102c <fun3+0x4>
    103c:   00002000    

00001040 <fun4>:
    1040:   e59f3014    ldr r3, [pc, #20]   ; 105c <fun4+0x1c>
    1044:   e5933000    ldr r3, [r3]
    1048:   e3530000    cmp r3, #0
    104c:   012fff1e    bxeq    lr
    1050:   e3530000    cmp r3, #0
    1054:   012fff1e    bxeq    lr
    1058:   eafffffa    b   1048 <fun4+0x8>
    105c:   00002004    

00001060 <fun5>:
    1060:   e3a01003    mov r1, #3
    1064:   e3a02004    mov r2, #4
    1068:   e59f3008    ldr r3, [pc, #8]    ; 1078 <fun5+0x18>
    106c:   e5831000    str r1, [r3]
    1070:   e5832000    str r2, [r3]
    1074:   e12fff1e    bx  lr
    1078:   00002000    

0000107c <fun6>:
    107c:   e3a02004    mov r2, #4
    1080:   e59f3004    ldr r3, [pc, #4]    ; 108c <fun6+0x10>
    1084:   e5832000    str r2, [r3]
    1088:   e12fff1e    bx  lr
    108c:   00002004    

Disassembly of section .bss:

00002000 <vlock>:
    2000:   00000000    

00002004 <ulock>:
    2004:   00000000    

Первый - самый красноречивый.

00001000 <fun1>:
    1000:   eafffffe    b   1000 <fun1>

Будучи локальной переменной, которая инициализирована и является энергонезависимой, тогда компилятор может предположить, что он не изменит значение между обращениями, поэтому он никогда не изменится в цикле while, так что по сути это цикл while 1. Если бы начальное значение было нулевым, это было бы простым возвратом, поскольку оно никогда не может быть ненулевым, будучи неустойчивым.

fun2 - это локальная переменная, которую необходимо построить фрейм стека

Он делает то, что предполагается, что код пытался сделать, подождать этой разделяемой переменной, которая может измениться во время цикла

    1010:   e59d3004    ldr r3, [sp, #4]
    1014:   e3530000    cmp r3, #0
    1018:   1afffffc    bne 1010 <fun2+0xc>

так что он производит выборку и каждый раз проверяет, что он делает, через цикл.

fun3 и fun4 одинаковы, но более реалистичны, так как внешний по отношению к коду функции не собирается менять блокировку, поскольку неглобальный не имеет большого смысла для вашего цикла while.

102c:   e5923000    ldr r3, [r2]
1030:   e3530000    cmp r3, #0
1034:   012fff1e    bxeq    lr
1038:   eafffffb    b   102c <fun3+0x4>

для случая volatile fun3 переменная должна быть прочитана и протестирована в каждом цикле

1044:   e5933000    ldr r3, [r3]
1048:   e3530000    cmp r3, #0
104c:   012fff1e    bxeq    lr
1050:   e3530000    cmp r3, #0
1054:   012fff1e    bxeq    lr
1058:   eafffffa    b   1048 <fun4+0x8>

для того, чтобы энергонезависимое являлось глобальным, оно должно попробовать его один раз, очень интересно, что здесь сделал компилятор, подумать о том, почему он это сделал, но в любом случае вы можете видеть, что «цикл» повторно проверяет прочитанное значение хранится в регистре (не кэшируется), который никогда не изменится с помощью правильной программы. Функционально мы попросили его прочитать переменную только один раз, используя энергонезависимую, а затем проверять это значение бесконечно.

fun5 и fun6 также демонстрируют, что volatile требует, чтобы компилятор выполнил доступ к переменной в своем хранилище, прежде чем перейти к следующей операции / доступу в коде. Поэтому, когда volatile, мы просим компилятор выполнить два назначения, два хранилища. Когда энергонезависимый компилятор может оптимизировать первое хранилище и делать только последнее, как если бы вы смотрели на код в целом, эта функция (fun6) оставляет переменную равной 4, поэтому функция оставляет переменную равной 4.

Решение для x86 не менее интересно, repz retq повсюду (с компилятором на моем компьютере), не трудно понять, что это такое.

Ни бэкенды aarch64, x86, mips, riscv, msp430, pdp11, ни двойная проверка fun3 () не выполняются.

pdp11 на самом деле проще для чтения кода (нет ничего удивительного)

00000000 <_fun1>:
   0:   01ff            br  0 <_fun1>

00000002 <_fun2>:
   2:   65c6 fffe       add $-2, sp
   6:   15ce 03e7       mov $1747, (sp)
   a:   1380            mov (sp), r0
   c:   02fe            bne a <_fun2+0x8>
   e:   65c6 0002       add $2, sp
  12:   0087            rts pc

00000014 <_fun3>:
  14:   1dc0 0026       mov $3e <_vlock>, r0
  18:   02fd            bne 14 <_fun3>
  1a:   0087            rts pc

0000001c <_fun4>:
  1c:   1dc0 001c       mov $3c <_ulock>, r0
  20:   0bc0            tst r0
  22:   02fe            bne 20 <_fun4+0x4>
  24:   0087            rts pc

00000026 <_fun5>:
  26:   15f7 0003 0012  mov $3, $3e <_vlock>
  2c:   15f7 0004 000c  mov $4, $3e <_vlock>
  32:   0087            rts pc

00000034 <_fun6>:
  34:   15f7 0004 0002  mov $4, $3c <_ulock>
  3a:   0087            rts pc

(это не связанная версия)


cmp DWORD PTR [RSP - 8], 0. <--- Почему сравнение выполняется с 0, в то время как DWORD PTR [RSP - 8] содержит 999? </p>

Хотя истинное ложное сравнение означает, что оно равно нулю или не равно нулю

Почему DWORD PTR [RSP - 8] копируется в EAX и опять же, почему выполняется сравнение между 0 и EAX?

mov -0x8(%rsp),%eax
cmp 0,%eax
cmp 0,-0x8(%rsp)

as so.s -o so.o
so.s: Assembler messages:
so.s:3: Error: too many memory references for `cmp'

сравнить хочет зарегистрироваться. Таким образом, он считывает данные в регистр, поэтому может выполнять сравнение, поскольку не может выполнить сравнение между непосредственным доступом и доступом к памяти в одной инструкции. Если бы они могли сделать это в одной инструкции, они бы сделали.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...