Почему LOOP так медленно?

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

Вот эксперименты, которые я сделал сегодня. Я использовал Microsoft Visual Studio 2010. Моя операционная система - 64-битная Windows 8. Мои вопросы в конце.

Первый эксперимент:

Платформа: Win32
Режим: Отладка (чтобы отключить оптимизацию)

begin = clock();
_asm
{
    mov ecx, 07fffffffh
start:
    loop start
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

Выход: passed time: 3.583
(Число меняется немного с каждым прогоном, но это морально того же размера.)

Второй эксперимент:

Платформа: Win32
Режим: Отладка

begin = clock();
_asm
{
    mov ecx, 07fffffffh
start:
    dec ecx
    jnz start
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

Выход: passed time: 0.903

Третий и четвертый эксперименты:

Просто измените платформу на x64. Поскольку VС++ не поддерживает 64-битную встроенную сборку, я должен поместить цикл в другой файл *.asm. Но, наконец, результаты одинаковы.

И с этого момента я начинаю использовать свой мозг - loop в 4 раза медленнее, чем dec ecx, jnz start, и единственная разница между ними, AFAIK, заключается в том, что dec ecx изменяет флаги, а loop - нет. Чтобы подражать этому флажку, я сделал

Пятый эксперимент:

Платформа: Win32 (в следующем я всегда полагаю, что платформа не влияет на результат)
Режим: Отладка

begin = clock();
_asm
{
    mov ecx, 07fffffffh
    pushf
start:
    popf
; do the loop here
    pushf
    dec ecx
    jnz start
    popf
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

Выход: passed time: 22.134

Это понятно, потому что pushf и popf должны играть с памятью. Но, скажем, например, что регистр eax не должен храниться в конце цикла (что может быть достигнуто путем упорядочивания регистров) и что флаг OF не нужен в цикле (это упрощает вещи, поскольку OF не находится в младших 8 бит flag), тогда мы можем использовать lahf и sahf для хранения флагов, поэтому я сделал

Шестой эксперимент:

Платформа: Win32
Режим: Отладка

begin = clock();
_asm
{
    mov ecx, 07fffffffh
    lahf
start:
    sahf
; do the loop here
    lahf
    dec ecx
    jnz start
    sahf
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

Выход: passed time: 1.933

Это гораздо лучше, чем использование loop напрямую, правильно?

И последний эксперимент, который я сделал, - это также попытаться сохранить флаг OF.

Седьмой эксперимент:

Платформа: Win32
Режим: Отладка

begin = clock();
_asm
{
    mov ecx, 07fffffffh
start:
    inc al
    sahf
; do the loop here
    lahf
    mov al, 0FFh
    jo dec_ecx
    mov al, 0
dec_ecx:
    dec ecx
    jnz start
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

Выход: passed time: 3.612

Этот результат является наихудшим, т.е. OF не задается в каждом цикле. И это почти то же самое, что непосредственно использовать loop...

Итак, мои вопросы:

  • Я прав, что единственное преимущество использования цикла в том, что он заботится о флажках (на самом деле только 5 из них, на которые действует dec)?

  • Существует ли более длинный вид lahf и sahf, который также перемещается OF, так что мы можем полностью избавиться от loop?

Ответ 1

Исторически, на процессорах 8088 и 8086 LOOP была оптимизацией, поскольку она занимала только один цикл дольше, чем условная ветвь, тогда как установка DEC CX перед ветвью стоила бы три или четыре цикла (в зависимости от состояния очередь предварительной выборки).

Сегодня процессоры работают по-разному по сравнению с 8086. Для нескольких поколений процессоров, несмотря на то, что производители сделали машины, которые могут правильно обрабатывать все документированные инструкции, которыми когда-либо владели 8088/8086 или ее потомки, они "Они сосредоточили свою энергию на повышении производительности только самых полезных инструкций. По ряду причин количество схем Intel или AMD должно было бы добавить к современному процессору, чтобы команда LOOP работала так же эффективно, как DEC CX/JNZ, вероятно, превысила бы общее количество схем на всем 8086, возможно, огромный запас. Вместо того, чтобы увеличивать сложность их высокопроизводительного процессора, производители включают в себя гораздо более простой, но более медленный процессор, который может обрабатывать" скрытые "инструкции. В то время как высокопроизводительному ЦП потребуется много схем, чтобы позволить выполнение нескольких инструкций перекрываться, за исключением случаев, когда более поздние инструкции нуждаются в результатах от более ранних вычислений (и должны ждать, пока они будут доступны)," блок управления неясными инструкциями" может избежать необходимо для такой схемы, просто выполняя инструкции по одному.