C / С ++. Почему простое целочисленное сложение в volatile может быть переведено в другую инструкцию asm для gcc и clang? - PullRequest
2 голосов
/ 25 октября 2019

Я написал простой цикл:

int volatile value = 0;

void loop(int limit) {
  for (int i = 0; i < limit; ++i) { 
      ++value;
  }
}

Я скомпилировал это с помощью gcc и clang (-O3 -fno-unroll-loops) и получил разные выходные данные. Они различаются по ++value части:

лязг:

  add dword ptr [rip + value], 1 # ++value
  add edi, -1                    # --limit
  jne .LBB0_1                    # if limit > 0 then continue looping

gcc:

  mov eax, DWORD PTR value[rip] # copy value to a register
  add edx, 1                    # ++i
  add eax, 1                    # increment a copy of value
  mov DWORD PTR value[rip], eax # store incremented copy to value, i. e. ++value
  cmp edi, edx                  # compare i < limit
  jne .L3                       # if i < limit then continue looping

C иВерсии C ++ одинаковы для каждого компилятора (https://gcc.godbolt.org/z/5x5jGP) Итак, мои вопросы:

1) GCC что-то делает не так? Какой смысл копировать value?

2) У меня есть бенчмаркированный этот код, и по какой-то причине профилировщик показывает, что в версии gcc 73% времени тратится на инструкцию add edx, 1, 13% на mov DWORD PTR value[rip], eax и 13% на cmp edi, edx. Я неправильно интерпретирую эти результаты? Почему другие инструкции по добавлению и перемещению занимают менее 1% времени?

3) Почему производительность может отличаться для gcc / clang в таком примитивном коде?

1 Ответ

7 голосов
/ 26 октября 2019

Это все потому, что вы использовали volatile, а GCC не оптимизирует его так агрессивно

Без энергозависимости, например, для одного ++*int_ptr вы получаете добавление к месту назначения памяти. (И, надеюсь, не inc при настройке для процессоров Intel; inc reg - это хорошо, но inc mem стоит лишних мопов против добавления 1. К сожалению, gcc и clang ошибаются и используют inc memс помощью -march=skylake: https://godbolt.org/z/_1Ri20)


clang знает, что он может сложить volatile доступ для чтения / записи в загрузочную и сохраненную части места назначения памяти add.

GCC не знает, как выполнить эту оптимизацию для volatile. Использование volatile в GCC обычно приводит к отдельной загрузке и сохранению mov, избегая возможности x86 сохранять размер кода с помощью CISCоперанды памяти для инструкций ALU На машине загрузки / хранения (как и любой RISC) вам все равно понадобятся отдельные инструкции загрузки и хранения, чтобы это не было проблемой.

TL: DR: различные внутренние компоненты компилятора вокруг volatile, в частности пропущенная оптимизация GCC.

Эта пропущенная оптимизация едва ли имеет значение, потому что volatile используется редко. Но вы можете сообщить об этом в bugzilla GCC, если хотите.

Без volatile, туалетр конечно бы оптимизировал прочь. Но вы можете увидеть один пункт назначения памяти add из GCC или clang для функции, которая просто выполняет ++*p.

1) GCC что-то делает неправильно? Какой смысл копировать значение?

Это только копирование в регистр . Обычно мы не называем это «копированием», а просто помещаем его в регистр, где он может работать с ним.


Обратите внимание, что gcc и clang также отличаются в том, как они реализуют условие цикла, с помощью clang. оптимизация только до dec / jnz (на самом деле add -1, но он будет использовать dec с -march = skylake или что-то с эффективным dec, то есть не Silvermont).

GCC тратит дополнительный уоп наусловие цикла (на процессорах Intel, где add/jnz может слиться в один макрос). IDK, почему он так наивно компилирует это.

73% времени тратится на инструкцию add edx, 1

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

add edx,1 ожидает перезагрузки value. С задержкой пересылки в 4–5 циклов это главное узкое место в вашем цикле.

(будь то между несколькими мопами места назначения памяти add или между отдельными инструкциямипо сути, не имеет значения. В вашем цикле нет других обращений к памяти, поэтому ни один из странных эффектов снижения задержки при пересылке магазина, если вы не пытаетесь слишком рано войти в игру: Добавление избыточного назначения ускоряет код, когдаскомпилировано без оптимизации или Цикл с вызовом функции быстрее, чем пустой цикл )

Почему другие инструкции добавления и перемещения занимают менее 1% времени?

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

3) Почему можнопроизводительность отличается от gcc / clang в таком примитивном коде?

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

...