Чтобы использовать встроенную сборку, вы должны использовать __asm__ volatile
.Однако этот тип оптимизации может быть преждевременным.То, что инструкций больше, не означает, что код работает медленнее - некоторые инструкции могут быть действительно медленными.Например, инструкция хранения BCD с плавающей запятой (fbstp
), хотя и по общему признанию, занимает более 200 циклов - по сравнению с одним циклом для простого mov
(Руководство по оптимизации от Agner Fog является хорошим ресурсом дляэти временные интервалы).
Итак, я реализовал несколько функций «подкачки», некоторые в C ++, а некоторые в сборке, и провел небольшое измерение, выполняя каждую функцию 100 миллионов раз подряд.
Контрольные примеры
std::swap
std::swap
, вероятно, является предпочтительным решением здесь.Он делает то, что вы хотите (поменяйте местами значения двух переменных), работает для большинства стандартных типов библиотек, а не только для целых чисел, четко передает то, что вы пытаетесь достичь, и переносим между архитектурами.
void std_swap(int *a, int *b) {
std::swap(*a, *b);
}
Вот сгенерированная сборка: она загружает оба значения в регистры, а затем записывает их обратно в противоположные области памяти.
movl (%rdi), %eax
movl (%rsi), %edx
movl %edx, (%rdi)
movl %eax, (%rsi)
XOR swap
Это то, что вы пытались сделать,в C ++:
void xor_swap(int *a, int *b) {
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
Это напрямую не переводит только в xor
инструкции, потому что на x86 нет инструкции, позволяющей напрямую xor
два места в памяти - вам всегда нужно загружатьпо крайней мере один из двух в регистр:
movl (%rdi), %eax
xorl (%rsi), %eax
movl %eax, (%rdi)
xorl (%rsi), %eax
movl %eax, (%rsi)
xorl %eax, (%rdi)
Вы также генерируете кучу дополнительных инструкций, потому что два указателя могут псевдоним , т.е. указывают на перекрывающиеся области памяти.Затем изменение одной переменной также приведет к изменению другой, поэтому компилятору необходимо постоянно сохранять и перезагружать значения. Реализация, использующая ключевое слово __restrict
для конкретного компилятора, будет компилироваться с тем же кодом, что и std_swap
(спасибо @ Ped7g за указание на этот недостаток в комментариях).
Поменяйте местами свременные переменные
Это «стандартный» обмен с временной переменной (который компилятор оперативно оптимизирует до того же кода, что и std::swap
):
void tmp_swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
Инструкция xchg
xchg
может поменять значение памяти со значением регистра - сначала это кажется идеальным для вашего случая использования.Тем не менее, это действительно медленно, когда вы используете его для доступа к памяти, как вы увидите позже.
void xchg_asm_swap(int *a, int *b) {
__asm__ volatile (
"movl (%0), %%eax\n\t"
"xchgl (%1), %%eax\n\t"
"movl %%eax, (%0)"
: "+r" (a), "+r" (b)
: /* No separate inputs */
: "%eax"
);
}
Нам нужно загрузить одно из двух значений в регистр, потому что тамдля двух ячеек памяти нет xchg
.
Своп XOR в сборке
Я сделал две версии свопа на основе XOR в сборке.Первое загружает только одно из значений в регистр, второе загружает оба, прежде чем их поменять местами и записать обратно.
void xor_asm_swap(int *a, int *b) {
__asm__ volatile (
"movl (%0), %%eax\n\t"
"xorl (%1), %%eax\n\t"
"xorl %%eax, (%1)\n\t"
"xorl (%1), %%eax\n\t"
"movl %%eax, (%0)"
: "+r" (a), "+r" (b)
: /* No separate inputs */
: "%eax"
);
}
void xor_asm_register_swap(int *a, int *b) {
__asm__ volatile (
"movl (%0), %%eax\n\t"
"movl (%1), %%ecx\n\t"
"xorl %%ecx, %%eax\n\t"
"xorl %%eax, %%ecx\n\t"
"xorl %%ecx, %%eax\n\t"
"movl %%eax, (%0)\n\t"
"movl %%ecx, (%1)"
: "+r" (a), "+r" (b)
: /* No separate inputs */
: "%eax", "%ecx"
);
}
Результаты
Вы можете просмотреть полноерезультаты компиляции вместе с сгенерированным кодом сборки на Godbolt .
На моей машине время (в микросекундах) немного различается, но обычно сопоставимо:
std_swap: 127371
xor_swap: 150152
tmp_swap: 125896
xchg_asm_swap: 699355
xor_asm_swap: 130586
xor_asm_register_swap: 124718
Вы можете видеть, что std_swap
, tmp_swap
, xor_asm_swap
и xor_asm_register_swap
, как правило, очень похожи по скорости - на самом деле, если я переместу xor_asm_register_swap
вперед, получится немного медленнее, чем std_swap
,Также обратите внимание, что tmp_swap
- это точно такой же код сборки, что и std_swap
(хотя он регулярно измеряется как немного быстрее, вероятно, из-за упорядочения).
xor_swap
, реализованный в C ++, немного медленнее, потому чтокомпилятор генерирует дополнительную загрузку / хранение памяти для каждой из инструкций из-за псевдонимов - как упоминалось выше, если мы изменим xor_swap
, чтобы вместо него взять int * __restrict a, int * __restrict b
(то есть, a
и b
никогда не псевдоним), компиляторгенерирует тот же код, что и для std_swap
и tmp_swap
.
xchg_swap
, несмотря на использование наименьшего количества инструкций, ужасно медленно (более чем в четыре раза медленнее, чем любой издругие варианты), просто потому что xchg
не является быстрой операцией, если она предполагает доступ к памяти.
В конечном счете, у вас есть выбор между использованием какой-либо пользовательской версии на основе сборки (которую трудно понять и поддерживать) или просто использованием std::swap
(что в значительной степени противоположно, а также преимуществами любых оптимизаций, которые стандартная библиотекадизайнеры могут придумать, например, использовать векторизацию для больших типов).Поскольку это более ста миллионов итераций, должно быть ясно, что потенциальное улучшение при использовании ассемблерного кода здесь очень мало - если вы улучшите его вообще (что не ясно), вы сэкономите максимум пару микросекунд.
TL; DR : Вы не должны этого делать, просто используйте std::swap(a, b)
Приложение: __asm__ volatile
Я понял, чтона этом этапе может иметь смысл немного объяснить встроенный код сборки.__asm__
(в режиме GNU достаточно asm
) вводит блок кода сборки.volatile
предназначен для того, чтобы компилятор не оптимизировал его - ему просто нравится просто удалять блок.
Существует две формы __asm__ volatile
.Один из них также имеет дело с goto
метками;Я не буду обращаться к этому здесь.Другая форма принимает до четырех аргументов, разделенных двоеточиями (:
):
- Простейшая форма (
__asm__ volatile ("rdtsc")
) просто сбрасывает код сборки, но на самом деле не взаимодействует с кодом C ++вокруг него.В частности, вам нужно угадать, как переменные присваиваются регистрам, что не совсем хорошо. - Обратите внимание, что инструкции кода сборки разделены
"\n"
, потому что этот код сборки дословно передается ассемблеру GNU.(gas
). - Второй аргумент - это список выходных операндов .Вы можете указать, какой «тип» у них есть (в частности,
=r
означает «любой операнд регистра», а +r
означает «любой операнд регистра, но он также используется как вход»).Например, : "+r" (a), "+r" (b)
указывает компилятору заменить %0
(ссылается на первый из операндов) регистром, содержащим a
, и %1
регистром, содержащим b
. - Это обозначениеозначает, что вам нужно заменить
%eax
(как вы обычно ссылаетесь на eax
в обозначении сборки AT & T) на %%eax
для экранирования знака процента. - Вы также можете использовать
".intel_syntax\n"
для переключения на сборку Intelсинтаксис, если вы предпочитаете. - Третий аргумент тот же, но имеет дело с операндами только для ввода.
- Четвертый аргумент сообщает компилятору, какие регистры и ячейки памяти теряют свои значения, чтобы включить оптимизацию вокругкод сборки.Например, «clobbering»
"memory"
, скорее всего, побудит компилятор вставить полный забор памяти.Вы можете видеть, что я добавил в этот список все регистры, которые я использовал для временного хранения.