Правильный способ реализации встроенного ассемблера в c ++ для операций xor над переменными - PullRequest
0 голосов
/ 17 февраля 2019

Недавно я видел статью о том, как операция подкачки может быть выполнена с использованием xor'ing вместо использования временной переменной.Когда я компилирую код, используя int a ^= b;, результат не будет просто (для синтаксиса at & t)

xor b, a
etc.

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

Я пробовал несколько ключевых слов, таких как:

asm(...);
asm("...");
asm{...};
asm{"..."};
asm ...
__asm ...

Ничего из этого не сработало, либо выдает синтаксическую ошибку, gcc, похоже, не принимает весь этот синтаксис, либо говорит:

main.cpp: Assembler messages:
main.cpp:12: Error: too many memory references for `xor'

По сути, я хочу использовать переменные, определенные в моем c ++код, используемый в блоке ассемблера, использующий три строки для их преобразования в Xor, а затем меняющие переменные в основном так:

int main() {
    volatile int a = 5;
    volatile int b = 6;
    asm {
        xor a,b
        xor b,a
        xor a,b
    };
    //a should now be 6, b should be 5
}

Чтобы уточнить: я хочу избежать использования сгенерированных компилятором операций mov, поскольку они требуют больше ресурсов процессорациклов, чем просто сделать три операции XOR, которые будут занимать три циклаКак я мог сделать это?

1 Ответ

0 голосов
/ 17 февраля 2019

Чтобы использовать встроенную сборку, вы должны использовать __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", скорее всего, побудит компилятор вставить полный забор памяти.Вы можете видеть, что я добавил в этот список все регистры, которые я использовал для временного хранения.
...