Обмен переменных со вспомогательной переменной и без нее — что быстрее?

Думаю, вы все слышали о «проблеме свопа»; SO полон вопросов об этом. Вариант подкачки без использования третьей переменной часто считается более быстрым, так как, ну, у вас на одну переменную меньше. Я хотел знать, что происходит за кулисами, и написал следующие две программы:

int main () {
    int a = 9;
    int b = 5;
    int swap;

    swap = a;
    a = b;
    b = swap;

    return 0;
}

и версия без третьей переменной:

int main () {
    int a = 9;
    int b = 5;

    a ^= b;
    b ^= a;
    a ^= b;

    return 0;
}

Я сгенерировал ассемблерный код с помощью clang и получил это для первой версии (в которой используется третья переменная):

...
Ltmp0:
    movq   %rsp, %rbp
Ltmp1:
    movl   $0, %eax
    movl   $0, -4(%rbp)
    movl   $9, -8(%rbp)
    movl   $5, -12(%rbp)
    movl   -8(%rbp), %ecx
    movl   %ecx, -16(%rbp)
    movl   -12(%rbp), %ecx
    movl   %ecx, -8(%rbp)
    movl   -16(%rbp), %ecx
    movl   %ecx, -12(%rbp)
    popq   %rbp
    ret
Leh_func_end0:
...

и это для второй версии (которая не использует третью переменную):

...
Ltmp0:
    movq    %rsp, %rbp
Ltmp1:
    movl   $0, %eax
    movl   $0, -4(%rbp)
    movl   $9, -8(%rbp)
    movl   $5, -12(%rbp)
    movl   -12(%rbp), %ecx
    movl   -8(%rbp), %edx
    xorl   %ecx, %edx
    movl   %edx, -8(%rbp)
    movl   -8(%rbp), %ecx
    movl   -12(%rbp), %edx
    xorl   %ecx, %edx
    movl   %edx, -12(%rbp)
    movl   -12(%rbp), %ecx
    movl   -8(%rbp), %edx
    xorl   %ecx, %edx
    movl   %edx, -8(%rbp)
    popq   %rbp
    ret
Leh_func_end0:
...

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

Какая из приведенных выше версий свопинга переменных быстрее и требует меньше памяти?


person shutefan    schedule 19.12.2011    source источник
comment
Чтобы узнать, что быстрее, почему бы вам не протестировать его?   -  person Dan Fego    schedule 20.12.2011
comment
Я бы не знал, как измерить использование памяти, плюс меня также интересуют причины этого.   -  person shutefan    schedule 20.12.2011
comment
Не похоже, что вы скомпилировали с включенной оптимизацией. В этой сборке много пуха.   -  person Drew Dormann    schedule 20.12.2011
comment
Трюк XOR возможно был быстрее много лет назад, но не на современных процессорах. Просто используйте временную переменную и включите оптимизацию компилятора.   -  person Blastfurnace    schedule 20.12.2011
comment
Бессмысленно пытаться судить о скорости сгенерированного компилятором кода и опускать оптимизацию.   -  person Gunther Piez    schedule 20.12.2011


Ответы (3)


Посмотрите на какую-нибудь оптимизированную сборку. Из

void swap_temp(int *restrict a, int *restrict b){
    int temp = *a;
    *a = *b;
    *b = temp;
}

void swap_xor(int *restrict a, int *restrict b){
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

gcc -O3 -std=c99 -S -o swapping.s swapping.c произведено

    .file   "swapping.c"
.text
.p2align 4,,15
.globl swap_temp
.type   swap_temp, @function
swap_temp:
.LFB0:
.cfi_startproc
movl    (%rdi), %eax
movl    (%rsi), %edx
movl    %edx, (%rdi)
movl    %eax, (%rsi)
ret
.cfi_endproc
.LFE0:
.size   swap_temp, .-swap_temp
.p2align 4,,15
.globl swap_xor
.type   swap_xor, @function
swap_xor:
.LFB1:
.cfi_startproc
movl    (%rsi), %edx
movl    (%rdi), %eax
xorl    %edx, %eax
xorl    %eax, %edx
xorl    %edx, %eax
movl    %edx, (%rsi)
movl    %eax, (%rdi)
ret
.cfi_endproc
.LFE1:
.size   swap_xor, .-swap_xor
.ident  "GCC: (SUSE Linux) 4.5.1 20101208 [gcc-4_5-branch revision 167585]"
.section    .comment.SUSE.OPTs,"MS",@progbits,1
.string "Ospwg"
.section    .note.GNU-stack,"",@progbits

На мой взгляд, swap_temp выглядит настолько эффективным, насколько это возможно.

person Daniel Fischer    schedule 19.12.2011
comment
Красавчик, спасибо за оптимизацию! Это так быстро/коротко? Кстати, есть ли разница, если я поменяю местами указатели вместо переменных? - person shutefan; 20.12.2011
comment
Осмелюсь сказать, что swap_temp оптимален. Для swap_xor без квалификаторов restrict gcc генерирует на одну инструкцию меньше, становится три movl a, b; xorl c, d, где в каждой операции один из аргументов является регистром (%eax, всегда), а один — разыменованием указателя ((%rsi) или (%rdi)). По моим замерам, медленнее (но если функция видна на месте вызова, инлайнинг может убрать разницу). Что касается разницы между заменой переменных и заменой указателей, переменные подкачки никогда не могут быть скрыты, поэтому оптимизация часто может полностью устранить ее. - person Daniel Fischer; 20.12.2011

Проблема с трюком подкачки XOR заключается в том, что он строго последователен. Это может показаться обманчиво быстрым, но на самом деле это не так. Есть инструкция под названием XCHG, которая меняет местами два регистра, но она также может быть медленнее, чем простое использование 3 MOVs, из-за своей атомарной природы. Обычная техника с темпом - отличный выбор ;)

person ScarletAmaranth    schedule 20.12.2011
comment
-1 нет проблем с синхронизацией с xchg reg1, reg2. Проблемы с синхронизацией возникают только с xchg с операндами памяти. - person Johan; 01.04.2014
comment
@Johan Йохан, хорошо, приятно знать, спасибо! :) (я исправил ответ) - person ScarletAmaranth; 01.04.2014
comment
Вы отредактировали ответ, но он все еще неверен. XCHG reg, reg нет проблем с атомарностью и поэтому не медленнее, чем 3 MOVs. В зависимости от процессора XCHG может (или не может) разбиваться на несколько микроопераций. Только XCHG reg,[reg] (который обменивает регистр с ячейкой памяти) работает медленно, потому что к нему прикреплен неявный префикс LOCK. Это префикс LOCK, который замедляет работу. - person Johan; 02.04.2014

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

movl   -12(%rbp), %ecx

В этой строке потребуется что-то вроде единицы времени для доступа к значению в регистре ecx, одна единица времени для доступа к rbp, еще одна для применения смещения (-12) и еще единицы времени (скажем, произвольно 3) для перемещения значения из регистра. адрес, хранящийся в ecx, на адрес, указанный от -12(%rbp).

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

person Vosobe Kapsimanis    schedule 19.12.2011
comment
Это верно в данном случае, но не в целом, поскольку игнорирует возможности конвейерной обработки. - person gnometorule; 20.12.2011
comment
Согласен, но тогда вы должны знать, как оптимизировать свой код для оптимизации конвейерной обработки и минимизации ветвления. Я думаю, что нашему другу проще начать с минимизации ненужных ссылок и избыточных команд, а затем перейти к более продвинутым методам. - person Vosobe Kapsimanis; 20.12.2011