Страницы

Поиск по вопросам

суббота, 28 декабря 2019 г.

Что эффективней: inc eax или add eax,1?

#ассемблер


Начал изучать ассемблер, знаю C/C++. Поэтому решил отталкиваться от знаний, которые
уже есть, и начал дизассемблировать мною написанный код и смотреть как что устроено.
И сразу же наткнулся на нелогичный момент.
Если в ассемблере есть функция увеличения значения аргумента inc eax, то она должна
отличаться от прибавления 1 к регистру путем add eax,1.
Я обычно предполагал, что если есть отдельные такие функции, то они эффективнее...
но если смотреть код i++:
mov eax,dword ptr [i]
add eax,1
mov dword ptr [i],eax

Что лучше использовать: inc или add в таком случае?
P.S. Использую Visual Studio 2008.    


Ответы

Ответ 1



Я думаю, что компилятор сглупил. Вероятно сказывается тот факт, что теоретически результат i++ ещё может быть потом использовать и из-за этого он пытается сохранить результат в r\eax. На самом деле компилятор мог бы сгенерировать просто inc dword ptr [i] Кроме того, здесь явно целых три инструкции вместо одной и это уж точно медленнее (если только значение i++ не нужно потом). Что же до явного сравнения add eax, 1 и inc eax, то подозреваю, что разницы в скорости никакой, но цифра 1 занимает место, а это означает, что она займёт место в очереди предвыборки кода и может сыграть отрицательную роль. А может и не сыграть. С другой стороны, компилятор действительно упорно использует add eax, 1: #include int main(void) { int i = atoi("7"); return ++i; } Даёт результат без inc'а 00000000004004b0
: 4004b0: 48 83 ec 08 sub $0x8,%rsp 4004b4: bf bc 05 40 00 mov $0x4005bc,%edi 4004b9: 31 c0 xor %eax,%eax 4004bb: e8 e8 fe ff ff callq 4003a8 4004c0: 48 83 c4 08 add $0x8,%rsp 4004c4: 83 c0 01 add $0x1,%eax С другой стороны, Java JIT-компилятор использует иногда inc.. сам видел.. UPD 2 kiralagin: Читайте в официальной доке Intel, страница 341.

Ответ 2



Всё дело в том, что вы компилируете в режиме Debug. Найдите переключатель и установите в релизный вариант)) Да, вы правы, инкремент гораздо быстрее. Просто в дебаговом варианте делается всё максимально "в лоб", дабы никто не запутался. А в релизном вы будете удивлены странностью команд (сам экспериментировал, приятно удивило). Инкремент быстрее потому что короче, меньше из памяти при выполнении читать. Это вроде основное преимущество. На то, что режим дебаговый, указывает и то, что перед изменением данные считываются из памяти, а потом записываются назад, что заметно дольше самой операции. Плюс, опыт, ибо сам когда-то столкнулся и нашёл причину;)

Ответ 3



Ну Вы и вопросы, конечно, задаёте! Это же всё настолько железо-зависимые вещи! Общее правило простое: в плане генерации ассемблерного кода компилятор всегда умнее (осведомлённее), чем Вы. Если он делает так, значит, так будет лучше. Вторая мысль: Вы основываетесь на ложном посыле. Введение новых машинных команд далеко не всегда диктуется эффективностью вычислений. Есть же ещё такая вещь, как эффективность программистов — согласитесь, inc eax написать гораздо проще, чем add eax,1 ;). А операция крайне частая. Теперь по поводу того, почему add быстрее, чем inc. (Всё что я сейчас напишу не обязано являться правдой — просто мне так кажется). Дело в том, что inc, в отличие от add, не изменяет состояние флага переноса. А некоторые другие флаги изменяет. Поскольку мне не известно ни одной архитектуры, на которой возможно выставлять флаги по одному, а не все скопом, то я пытаюсь утверждать следующее: inc'у потребуется сначала считать текущее состояние флага переноса (что порождает ложную зависимость с последующим простоем конвейера). А add'у это не требуется, поскольку он его перезаписывает. Отсюда и прирост производительности. P.S. Отказ от ответственности ещё раз: я сам в этом не очень хорошо пока разбираюсь (обращайтесь через полгодика — судя по всему, к тому времени я уже буду экспертом в этой области =) ). Просто где-то что-то подобное слышал\читал.

Ответ 4



gcc (GCC) 3.4.5 (mingw-vista special r3) использует incl для i++ j++ при компиляции с -O3 он вообще держит переменные в регистрах. gcc -S -O3 main () { int i = 0, j = 999; while (a(i)) { i++; j++; b(j); } } делает (для while) код: xorl %esi, %esi movl $999, %ebx jmp L2 .p2align 4,,7 L4: incl %ebx // j++ incl %esi // i++ movl %ebx, (%esp) // передача j в b() call _b L2: movl %esi, (%esp) // передача i call _a testl %eax, %eax // проверка результата, возвращаемого в регистре eax jne L4 По поводу скорости исполнения. Лично мне кажется, что в циклическом коде разумного размера для современной реализации архитектуры x86 скорость исполнения будет, как это ни странно на первый взгляд выглядит, одинаковой. Это связано с предвыборкой команд и их преобразованием к командам RISC-подобного ядра процессора.

Комментариев нет:

Отправить комментарий