Я разобрал код с помощью MSVC, чтобы лучше понять, что происходит. Оказывается, псевдонимы не были проблемой вообще, и при этом не была какая-то безопасность параноидальной нити.
Вот интересная часть функции arradd
отключена:
for (int n = 0; n < 10; ++n)
{
for (int i = 0; i < sz; ++i)
013C101C mov ecx,ebp
013C101E mov ebx,29B9270h
{
sm += arr[i];
013C1023 add eax,dword ptr [ecx-8]
013C1026 add edx,dword ptr [ecx-4]
013C1029 add esi,dword ptr [ecx]
013C102B add edi,dword ptr [ecx+4]
013C102E add ecx,10h
013C1031 sub ebx,1
013C1034 jne arradd+23h (13C1023h)
013C1036 add edi,esi
013C1038 add edi,edx
013C103A add eax,edi
013C103C sub dword ptr [esp+10h],1
013C1041 jne arradd+16h (13C1016h)
013C1043 pop edi
013C1044 pop esi
013C1045 pop ebp
013C1046 pop ebx
ecx
указывает на массив, и мы можем видеть, что внутренний цикл развернут здесь x4 - обратите внимание на четыре последовательные add
инструкции со следующих адресов и ecx
продвигается на 16 байт (4 слова) за раз внутри цикла.
Для неоптимизированной версии функции-члена doadd
:
int tester::doadd()
{
sm = 0;
for (int n = 0; n < 10; ++n)
{
for (int i = 0; i < sz; ++i)
{
sm += arr[i];
}
}
return sm;
}
Разборка (это сложнее найти, так как компилятор вставил ее в main
):
int tr_result = tr.doadd();
013C114A xor edi,edi
013C114C lea ecx,[edi+0Ah]
013C114F nop
013C1150 xor eax,eax
013C1152 add edi,dword ptr [esi+eax*4]
013C1155 inc eax
013C1156 cmp eax,0A6E49C0h
013C115B jl main+102h (13C1152h)
013C115D sub ecx,1
013C1160 jne main+100h (13C1150h)
Примечание 2 вещи:
- Сумма хранится в регистре -
edi
. Следовательно, здесь нет псевдонима "заботы" . Значение sm
не перечитывается постоянно. edi
инициализируется только один раз, а затем используется как временный. Вы не видите его возврата, так как компилятор оптимизировал его и использовал edi
непосредственно в качестве возвращаемого значения встроенного кода.
- Петля не развернута. Почему? Нет веской причины.
Наконец, вот «оптимизированная» версия функции-члена, в которой mysm
хранит локальную сумму вручную:
int tester::doadd_opt()
{
sm = 0;
int mysm = 0;
for (int n = 0; n < 10; ++n)
{
for (int i = 0; i < sz; ++i)
{
mysm += arr[i];
}
}
sm = mysm;
return sm;
}
(опять же, встроенная) разборка:
int tr_result_opt = tr_opt.doadd_opt();
013C11F6 xor edi,edi
013C11F8 lea ebp,[edi+0Ah]
013C11FB jmp main+1B0h (13C1200h)
013C11FD lea ecx,[ecx]
013C1200 xor ecx,ecx
013C1202 xor edx,edx
013C1204 xor eax,eax
013C1206 add ecx,dword ptr [esi+eax*4]
013C1209 add edx,dword ptr [esi+eax*4+4]
013C120D add eax,2
013C1210 cmp eax,0A6E49BFh
013C1215 jl main+1B6h (13C1206h)
013C1217 cmp eax,0A6E49C0h
013C121C jge main+1D1h (13C1221h)
013C121E add edi,dword ptr [esi+eax*4]
013C1221 add ecx,edx
013C1223 add edi,ecx
013C1225 sub ebp,1
013C1228 jne main+1B0h (13C1200h)
Цикл здесь развернут, но только х2.
Это довольно хорошо объясняет мои наблюдения за разницей в скорости. Для массива 175e6 функция выполняется ~ 1,2 с, неоптимизированный элемент ~ 1,5 с, а оптимизированный член ~ 1,3 с. (Обратите внимание, что это может отличаться для вас, на другой машине я получил более близкое время выполнения для всех 3 версий).
А как насчет gcc? При компиляции все 3 версии работали с ~ 1,5 сек. Подозревая отсутствие развёртывания, я посмотрел на разборку gcc
и действительно: gcc не разворачивает ни одну из версий .