Страницы

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

пятница, 11 января 2019 г.

Зачем при оптимизации копировать, инкрементировать, и копировать обратно?

Код ассемблера ниже, если включена оптимизация Maximize Speed:
; 30 : i = i + 1;
mov eax, DWORD PTR tv329[ebp] mov ecx, DWORD PTR tv328[ebp] inc ebx inc eax add ecx, 4 mov DWORD PTR tv329[ebp], eax mov DWORD PTR tv328[ebp], ecx
А этот код с оптимизацией Minimize Size
; 30 : i = i + 1;
inc edx inc esi add edi, 4
Операция в программе i = i + 1
Вопрос, зачем при оптимизации по скорости нужно копировать из DWORD PTR tv329[ebp] в eax, инкрементировать, а копировать обратно? Это помогает в скорости? Но как.
Обновление:
Maximize Speed: https://ideone.com/8JKULj Minimize Size: https://ideone.com/zbnv8n
Обновление 2: Оптимизация по скорости:
; 14 : do { ; 15 : i = g;
mov ebx, ecx xor eax, eax lea ecx, DWORD PTR _arr$[ebp+ecx*4] mov DWORD PTR tv329[ebp], eax mov DWORD PTR tv328[ebp], ecx npad 1
Оптимизация по размеру:
; 14 : do { ; 15 : i = g;
lea edi, DWORD PTR _arr$[ebp] mov edx, eax xor esi, esi lea edi, DWORD PTR [edi+eax*4]
Зачем в оптимизации по размеру в lea заносить один адрес, потом выполнять действия, не связанные с lea и с _arr$, а затем опять заносить в lea уже другой адрес?


Ответ

На вопрос в заглавии ответ - инкрементировать непосредственно в памяти процессор не умеет, поэтому надо вытащить значение в регистр, увеличить и положить в память обратно.
Конкретно приведенный в тексте вопроса кусок разумеется работает дольше, чем если бы i была просто в регистре. Но если рассмотреть весь код целиком, то выясняется, что переменная i по факту в цикле практически не используется и что самое важное - она не используется для обращения к массивам по индексу.
Зато в полном оптимизированном по скорости коде видно, что основные переменные по которым идут обращения к массиву - постоянно в регистрах, флажок bool c даже в регистре dl. В целом количество обращений к памяти в самой середине циклов (которые выполняются больше всего раз) меньше. Обращения к памяти для работы с флагом c так же отсутствуют.
Сравните например if в 20 строке и c=falseв 21:
mov edi, DWORD PTR _arr$[ebp+eax*4] mov eax, DWORD PTR _arr$[ebp+ecx*4] mov esi, DWORD PTR [ecx] mov DWORD PTR _tmp$1$[ebp], eax mov eax, DWORD PTR [ebx] cmp edi, esi cmp DWORD PTR _tmp$1$[ebp], eax jg SHORT $LN3@main jg SHORT $LN3@main
; 21: c=false xor dl, dl mov BYTE PTR _c$1$[ebp], 0
При оптимизации по скорости загружаем данные из 2х ячеек памяти, сравниваем и переходим. При оптимизации по размеру два "лишних" обращения к памяти. Причем часть из них самого if не касается, это похоже нужно позже, просто оптимизатор решил нарисовать их здесь. И в отличии от куска кода который не понравился вам, данная строка находится в 3 по вложенности цикле и выполняется явно гораздо больше раз, чем пресловутое i++; А флажок c в первом случае вообще мгновенный, во втором это загрузка константы по адресу в памяти, один из самых медленных вариантов mov, а ведь его потом еще проверяют так же доставая из памяти ...
Итого - при оптимизации по скорости самые важные переменные постоянно в регистрах, а под менее важную переменную i уже не хватило регистров, поэтому ее оставили в памяти и обращаются по мере необходимости.
P.S. А вообще оптимизатор жутко умный. я даже немного не ожидал от него такого (выдержки из вашего кода):
int n=10; ... n = n - 1; mov DWORD PTR _arr$[ebp+4], 9 " Он понял, что в память надо сразу занести 9, а не пытаться грузить 10 и вычитать" while (i <= n) cmp ebx, 9 " Он видит что n нигде не меняется и использует его как константу"
g = (n + 1) / 2; mov ecx, 5 " Он знает что n константа (9), значит g=5, вычислять опять не надо"
do { i = g; xor eax, eax do { ; В eax он будет далее в цикле вести j j = i - g; и поэтому перед циклом он просто делает ее 0 " Он понял, что на первой итерации i=g и не пытался вычислять j через i и g"
if(arr[j+g]... mov esi, DWORD PTR [ecx] " Вот это вообще гениально: он помнит, что в начале j=0 а в ecx он заранее загрузил адрес массива + g*4 (4 это размер элемента массива int) И когда вы пишите arr[j+g] он просто лезет в память по регистру ecx"
j = j - 1; dec eax ; В eax j - тут все ясно, вычли 1 sub ecx, 4 ; А это зачем ??? "Я все никак не мог понять, почему при вычитании 1 из j еще 4 вычитается из ecx Но ларчик просто открывался, в ecx у нас, как мы помним, адрес элемента массива arr с индексом [j+g], а т.к. j уменьшается, а g в обозримом будущем меняться не собирается, то мы просто вычитаем из адреса 4, т.е. размер одного элемента массива и таким образом нам в цикле вычислять arr+i+g вообще не надо, мы его всегда знаем"
"В приведенном коде сравнения 2х оптимизаторов был if(arr[j]<=arr[j+g]) который в конечном счете был cmp esi,edi; Так вот, значения элементов массива в регистрах далее по тексту не пропадает:" tmp = arr[j]; arr[j] = arr[j+g]; mov DWORD PTR _arr$[ebp+eax*4], esi arr[j+g] = tmp; mov DWORD PTR [ecx], edi "Нашей переменной tmp вообще нет, оптимизатор посчитал, что она не нужна и просто занес уже имеющиеся в регистрах значения arr[j] и arr[j+g] в массив, а в варианте с оптимизацией по объему tmp была ... "
Дополнение на основе дополнения в вопросе :)
lea edi, DWORD PTR _arr$[ebp] mov edx, eax xor esi, esi lea edi, DWORD PTR [edi+eax*4]
Почему он выбирает именно такой порядок инструкций сказать сложно, на размер это ни как не влияет, может влиять например на кеш данных у процессора, или на параллельность выполнения команд. И lea не в себя грузит адрес, а грузит его в указанный регистр, в данном случае edi. Первый lea грузит в edi адрес массива arr, а второй lea грузит адрес уже с использованием того edi, который подготовил первый lea. А конкретно он выполняет edi=edi+eax*4, ну т.е. второй lea эквивалентен четырем подряд add edi,eax, но разумеется быстрее и меньше в размере

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

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