Pull to refresh

Comments 55

Да, совсем недавно коллега сталкивался с подобным случаем, когда убрав из куска кода (написанного под компиляторы ещё плохо умеющие оптимизировать код) различные оптимизации, получил прирост производительности в 20%.
И всё как бы ничего, однако меня всё же насторожила такая казалось бы пустячная «оптимизация» с уклоном в арифметику.

Помимо того, что не следует смешивать разные блоки кода, и операторы ветвления в один блок, используя арифметические псевдооптимизации — понятно, что это общее правило, и долго объяснять, почему это так — конкретно в этом коде серьезный залет, который не приводит ни к какой оптимизации, а только вредит.


В исходном коде были оператор ветвления и сложение, а в новом… еще и умножение.


(На этом можно и закончить, и не говорить о таких пустяках, что в новом коде почему то и операторов сложения больше, и операторы вычитания появились...)

операторы сложения? где? между константами? учить мат-часть…
Уважаемый, в приведенном вами коде
index = opnd + (opnd <= TCL_INDEX_END)*(objc - 1 - TCL_INDEX_END);

нет оператора сложения (как и умножение, так и вычитания) между константами, кроме «1 — TCL_INDEX_END»

Надеюсь, не будете выкручиваться, утверждая, что opnd и objc — константы.

Так что учите матчасть сами.

там у вас не стояло слово «лишний“ разве? если нет, тогда звиняйте (уже перебрал значит)…
Только умножение он хотел вместо условного перехода (то что вы назвали „оператор ветвления“), так что — ваше „ещё и“ все равно мимо.
Проблема в том, что усножение — весьма дорогая операция. А ветвление часто можно на cmov заменить.

На старых процессорах без cmov оптимизация не имеет смысла. На новых процессорах с cmov — тем более.

Так где оно может, хотя бы теоретически, иметь смысл???
Проблема в том, что умножение — весьма дорогая операция.

Да ну? Тот же imul в интелях всего с максимально 3 cycle latency если мне не изменяет память.
А условный переход, он как привило длиннее (в реальности).


И потом у ув. профессора Портера здесь было умножение на 0/1, т.е. как-раз как бы с надеждой заменить при компиляции на что-нибудь более подходящее.


Просто он забыл, что компиляторы давно и буквально следуют первому правилу при процессорной оптимизации (машинного кода):


Always avoid a branch if you can.

Т.е. что условный переход на таком коротком if тоже не получится...


Ну и еще забылось что имеем: branch prediction / ILP, instruction reordering, instruction, TLB and/or data cache (hits/misses/page faults) т.д., и register renaming и прочее впридачу.
Работать компилятором нынче очень сложно. :)


Так где оно может, хотя бы теоретически, иметь смысл???

Я видел вполне себе хорошую реализацию switch на imul для коротких выравненных кусков кода. Даже очень. Побило всё остальное, вплоть до многоуровневых jump-tables, и прочие дериваты.
Другое дело что оно, так-то, не универсально конечно.


И не забывайте, что кэш процессора ограничен. Часто Ofast оптимизация, создающая код чуть длиннее чем O2, много быстрее на искусственных тестах, пока в бою не проявляются проблемы на multithreading, паразитной нагрузке и т.п.

Да ну? Тот же imul в интелях всего с максимально 3 cycle latency если мне не изменяет память.
Изменяет. Pentium — 9/11, более старые процессоры — ещё медленнее. И даже на Skylake может до 4 циклов занимать.

Но с учётом того, что на процессорах новее Pentium Pro есть cmov критическое число — 9.

И потом у ув. профессора Портера здесь было умножение на 0/1, т.е. как-раз как бы с надеждой заменить при компиляции на что-нибудь более подходящее.
Напишите тогда уж XOR, тогда есть шансы.

Просто он забыл, что компиляторы давно и буквально следуют первому правилу при процессорной оптимизации (машинного кода):
Always avoid a branch if you can.
Давно, но не буквально. Это мы уже обсуждали.

Я видел вполне себе хорошую реализацию switch на imul для коротких выравненных кусков кода. Даже очень. Побило всё остальное, вплоть до многоуровневых jump-tables, и прочие дериваты.
Я, наверное, ничего не понимаю в колбасных обрезках, но в подобных случаях ведь умножать нужно на константу, а не на переменную… то есть это lea, а не imul… сложение, а не умножение…
> И даже на Skylake может до 4 циклов занимать.

2-operand form, при умножении на 0/1? Вы серьезно?

> Я, наверное, ничего не понимаю в колбасных обрезках

Наверное… :) выше под умножением понималось умножение в сишном коде, как его уже реализует компилятор не очень интересно.

Конкретно в контексте того упомянутого `switch`, ужо не упомнить (а думать некогда), но чисто для примера:
```asm
; в EAX передается значение для switch-а…
; дефоулт всё что больше 50, размер каждой ветки (паддинг) = 24
cmp EAX, 50
ja switch_default
imul EAX, const_padding_size ;# EAX *= 24
jmp EAX
sw0th:
...; instr (24 bytes)
sw1st:
...; instr (24 bytes)
sw2nd:
...; instr (24 bytes)

sw50th:
...; instr (24 bytes)
switch_default:
ret
```
2-operand form, при умножении на 0/1? Вы серьезно?
При умножении на 0/1 — скорее всего нет. Я не знаю как оно у них зависит от аргументов. У Agnerа таблицы не настолько детализованы.

Наверное… :) выше под умножением понималось умножение в сишном коде, как его уже реализует компилятор не очень интересно.
Не очень понятно как сишный код сочетать с «короткими выравненными кусками кода» — это уже явно ассемблерный уровень.

imul EAX, const_padding_size ;# EAX *= 24
Тут я бы два LEA поставил. Или LEA + SAL, как компилятор делает:
$ cat test.c
int foo(int x) {
  return x*24;
}
$ gcc test.c -march=native -O3 -S -o-
	.file	"test.c"
	.text
	.p2align 4,,15
	.globl	foo
	.type	foo, @function
foo:
.LFB0:
	.cfi_startproc
	leal	(%rdi,%rdi,2), %eax
	sall	$3, %eax
	ret
	.cfi_endproc
.LFE0:
	.size	foo, .-foo
	.ident	"GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516"
	.section	.note.GNU-stack,"",@progbits
Тут та же самая петрушка, что и с переходами. Не всё то, что выглядит как умнодение в C является умножением на ассемблере.

P.S. А вообще ваш трюк, как и многое подобное, не нов и не уникален. GCC старых версий подобное творит. С lea, конечно, не с imul. Впрочем последние версии не заморачиваются
> int index;
> if ((index = opnd) <= (-2)) {

В чём проблема инициализировать index сразу значением opnd в примере test2? (int index = opnd) Вы приводите аргументы против смешивания математики и flow в одном выражении, но при этом именно это и делаете в test2.
В чём проблема инициализировать index сразу значением opnd

Как раз это компилятору все равно, а в такой форме оно нагляднее, и речь тут не о том… Да и в оригинальной функции оно не так...


но при этом именно это и делаете в test2.

Совершенно нет, и при "ассемблерном мышлении" как раз проще — результат присвоения в eax, потом cmp eax, и условное действие (переход или присвоение-пересылка).


Все дело в том, что условный переход действие не дешевое, и раньше их часто заменяли, умножением в том числе...

что там компилятор наоптимизировал, выходит иногда боком при исполнении на современном железе с собственной оптимизацией внутри, типа внеочередное или спекулятивное исполнение, параллелизм на уровне команд (ILP) и прочее.

Опция -m arch=686 должна в этом случае помогать.
количество комманд ассемблера не показатель, напиши этот код в большом цикле и посмотри что быстрее, мне например не нравится умножение как дорогая операция, я бы сделал

index = opnd + (~((opnd<-1)-1))&(1 + objc)
короче быстрее всех просто index = (opnd < -1)? opnd: opnd+1+indexEnd;
вариант с магией битов 2 место, с умножением — 3 место
условный переход всегда медленней умножения. Иногда медленнее на порядки если процессор неправильно предскажет ветвление.
да вроде нет, цикл на 100 миллионов операций 10% быстрее работает паскуда, то есть ещё несколько быстрее за вычетом других комманд, а фокус с битами на 3-5% быстрее умножения, или я запускаю как то не так
Разница будет зависеть от целевой архитектуры, используемого компилятора, опций компиляции, конкретного процессора, и т.д. и т.п. — нельзя сказать, что «ветвление однозначно быстрее битовых фокусов» или наоборот.
Вообще нет. Правильно предсказанный переход — это вообще 0 тактов. Вот неправильно предсказанный — дело другое.

Но при этом нужно учитывать, что условный оператор != условный переход.
А разве результат сравнения True гарантированно будет равен 1? Я думал там может получиться любое ненулевое число.

В C нет типа Boolean. Типом результата сравнения является int.


The == (equal to) and != (not equal to) operators are analogous to the relational
operators except for their lower precedence.108) Each of the operators yields 1 if the
specified relation is true and 0 if it is false. The result has type int. For any pair of
operands, exactly one of the relations is true.


http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
Страница 96.

Конструкция "if(foo)" равносильна "if(foo!=0)". Именно поэтому любое ненулевое выражение является истинным.

Не просто 1, но еще и типа int, как правильно сказали выше. И вообще, вы из матана забыли, что функция должна уметь брать все числа из области определения и выдавать одно и только одно (детерминированое) значение, если последнее не так, то это уже Многозна́чная фу́нкция. ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%BE%D0%B3%D0%BE%D0%B7%D0%BD%D0%B0%D1%87%D0%BD%D0%B0%D1%8F_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F

А тут вы вообще хотите, что бы она любое значение выдала, ну вы даете. Ей для этого, что генератор псевдослучайных чисел нужен? Ну или случайный через генератор на физ. феномене?
Про сравнение я понял, что ошибся.

Но функции в языках программирования не обязательно подчиняется математическим определениям, например:
int foo(int x, int y) { return x % y; }

В C89 для (3, -2) может вернуть как 1, так и -1.

Генератор псевдослучайных чисел тут не нужен. Просто если бы разработчики стандарта сказали, что результат сравнения — любое ненулевое число, то разработчики компилятора могли бы начать экономить такты, к примеру, вместо использования единицы копируя указатель на вершину стека — просто потому, что компилятор знает, что здесь он не ноль, а на данной архитектуре копирование указателя на вершину стека быстрее.

Это понятно, я просто преувеличил для эффекта))
Сталкивался с подобными эффектами при оптимизации ещё лет 10 назад и с того времени отношусь с осторожностью. Но такого как вы анализа не делал, спасибо за статью.
да разве то анализ… то просто один коммит, по моему неплохо подходящий в качестве примера
По этой причине эта функция еще и самая большая (порядка 6 тысяч строк + примитивы и макросы)

Сдается мне, не в оптимизациях там проблема.

Так не только вам сдается.
А использование -O3 и -Os как-нибудь повлияет на результат?
А можно другие компиляторы clang и VS так же себя ведут?
Под clang код test1 и test2 получается одинаковым.
Под gcc для AArch64 тоже получается одинаковым.
В общем, не настолько компиляторы глупы, как sebres их пытается выставить.
Главным аргументом при выборе между двумя вариантами должна быть читаемость кода, а не то, в какую последовательность команд он оттранслируется конкретной версией компилятора с конкретным набором флагов.
не настолько компиляторы глупы, как sebres их пытается выставить.

А нельзя ткнуть носом, где я пытаюсь их такими выставить?


Наоборот я их всегда, всячески хвалю, например — "Не стоит забывать про оптимизацию в современных компиляторах, которая достигла если не совершенства, то уже очень и очень на уровне."


И потом я же написал — "во всех вариантах (и даже с максимальной оптимизацией) test1 ничем не лучше, а то и проигрывает test2".


Про собственно clang же много историй можно рассказывать. Он у меня был любимый долгое время… даже кастомная собственная сборка с измененным поведением при оптимизации.
Потом я столько всего под clang словил, что окончательно забросил...

Я имел в виду:
смешивается в кучу математика и собственно flow процесса, что например усложняет разбор его для компилятора

--здесь уместнее было написать «усложнял», потому что более навороченным компиляторам уже и такой смешанный код по зубам.

Здесь конкретно да, а в других примерах ещё как посмотреть...


Про "более навороченным компиляторам" тоже могу поспорить.
Оно местами настолько "навороченно", что неявное (а то и неопределённое) поведение на нормальном течении словить можно (было как-то, долго боролся, помог откат на пред. версию).


Из последнего вот к примеру, expressive diagnostics у них вообще таким местом реализован, что варнинг на варнинге и третьим погоняет (даже без "-Wextra"). А если например в проекте варнинг к ошибке преравнивается?

Интересно, а если убрать index, который здесь совсем не нужен, а делать все операции над передаваемым параметром opnd, то результат оптимизации изменится?

А мне кажется, что самое неправильное в данном коммите — полное уничтожение читаемости. До коммита — ясно, что, почему и как, а после — сим-салябим, ахалай махалай, тыдыщ — ЧУДО!
Даже если бы оно работало быстрее, сокращение комментария вместо добавления обоснования применённого метода — чистой воды вредительство.

То есть в тексте о б этом сказано, но мимоходом, и на самом последнем месте. А по мне — надо на первом.

Я могу ошибаться, но 3 операции с памятью в test2 могут выполняться дольше двух в test1. И, следовательно, test2 будет медленнее, несмотря на то, что короче.
Поправьте меня если я ошибся.

Поправляю — там вообще нет операций с памятью, кроме загрузки значений операндов со стека (которые одинаковы в обоих вариантах). lea — не операция с памятью, несмотря на вид ее второго параметра.

Не хватает варианта "не занимаюсь пессимизацией кода прямо в процессе разработки".
Я за алгоритмическую оптимизацию — если есть возможность сделать из квадратичной сложности линейную или из линейной логарифмическую — почему нет?


Важно только помнить, что логарифмическая сложность не всегда быстрее линейной :-)

UFO just landed and posted this here
«Мохнатое О» не имеет отношения к смузи. У нас в университете (КФУ, ВМК) это в 90-х на первом курсе читали. Покойный Самитов сейчас в гробу вертится от вашего «смузихлёбства».
UFO just landed and posted this here
Вы это серьёзно что ли? О-большое — необходимая часть для понимания сложности алгоритмов и написания программ.
UFO just landed and posted this here
Топик описывает что нужно делать после того, как с «big O» всё в порядке.

Сколько я видел «сурово оптимизированных» программ с пресловутым алгоритмом маляра Шлемиэля… Не сосчитать. И они все легко «обходятся» совершенно неоптимизированным кодом, в котором пресловутого «Шлемиэля» нету.

А вот если вы всех «Шлемиэлей» извели — тогда уже можно и о тактах думать.
Не думаю, что в статье пример из этой области, но есть небольшая тонкость по поводу «убрать условные переходы». В криптографических алгоритмах стараются избегать всех условных переходов которые зависят от используемых данных (просто условный переход в цикле не в счёт). В таких случаях к чёрту оптимизацию в пару циклов, там важнее чтобы не было атак по сторонним каналам (по времени, например). Так что, в некоторых отдельных случаях такой подход полезен.
А вот, например, в исходниках драйвера amd для видеокарт запрещают более одного return в теле функции. Прикол, да?
UFO just landed and posted this here
UFO just landed and posted this here
При этом версия 3.6 лучше компилирует как раз «однозначно более быстрый вариант»… Так что разрабатывай вы это всё под Mac несколько лет назад — был бы повод добавить комментариев и эту версии принять.

Впрочем есть ощущение, что сегодня, сейчас, закладываться на более старую версию clang'а не стоит — тем более, что и icc и MSVC лучше обрабатывают test2.
В общем кто-то в очередной раз подумал, что он умнее компилятора, с предсказуемым результатом (и бонусом в виде обсуфкации кода).
Кто-то — это на минуточку профессор (в UNC)..., про «умнее» компилятора — это думаю однозначно (по крайней мере Дон уж точно их не одну тонну перелопатил вдоль и поперёк)..., сделал он этот «фикс» походя (не сильно напрягаясь сам, и вряд ли напрягая команду — ибо конкретно такие «обфускации» даже новички читают бегло)..., и результат вовсе не однозначно предсказуем/очевиден.
Т.е. напрасно — да, ИМХО возможно даже показательно (в качестве примера, отсюда статья).
Однако это писалось однозначно не для того, что бы кого-то тыкать носом.

В общем кто-то мог просто пройти мимо, не оставляя таких «капитанских» и очень содержательных комментариев.
П.С. Ну и наверно стоит всё же немного уважительней относится к людям, тратящим своё время и нервы на опенсорс…
Sign up to leave a comment.

Articles