Рассмотрим следующий код ( p
имеет тип unsigned char*
и bitmap->width
имеет некоторый целочисленный тип, который точно неизвестен и зависит от того, какую версию какой-либо внешней библиотеки мы используем):
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Стоит ли его оптимизировать [..]
Может ли быть случай, когда это может дать более эффективные результаты, написав:
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0; x < width; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
... или компилятор может оптимизировать это тривиально?
Какой код вы считаете «лучшим»?
Примечание редактора (Айка): для тех, кто интересуется зачеркнутым текстом, исходный вопрос, как он сформулирован, был опасно близок к территории вне темы и был очень близок к закрытию, несмотря на положительные отзывы. Они вычеркнуты. Тем не менее, пожалуйста, не наказывайте ответивших, обратившихся к этим затронутым разделам вопроса.
c++
performance
caching
optimization
strict-aliasing
Ярон Коэн-Тал
источник
источник
*p
имеет тот же тип, что иwidth
тогда, его нетривиально оптимизировать, поскольку онp
может указыватьwidth
и изменять его внутри цикла.p
указывает на ту же память, что иbitmap->width
. Поэтому я не могу юридически оптимизировать первый пример до второго.Ответы:
На первый взгляд мне показалось, что компилятор может сгенерировать эквивалентную сборку для обеих версий с активированными флагами оптимизации. Когда я его проверил, то с удивлением увидел результат:
Источник
unoptimized.cpp
примечание: этот код не предназначен для выполнения.
Источник
optimized.cpp
примечание: этот код не предназначен для выполнения.
компиляция
$ g++ -s -O3 unoptimized.cpp
$ g++ -s -O3 optimized.cpp
Сборка (unoptimized.s)
Сборка (optimized.s)
разница
Сгенерированная сборка для оптимизированной версии действительно загружает (
lea
)width
константу, в отличие от неоптимизированной версии, которая вычисляетwidth
смещение на каждой итерации (movq
).Когда у меня будет время, я в конце концов опубликую тест по этому поводу. Хороший вопрос.
источник
const unsigned
вместоunsigned
неоптимизированного случая.main
для тестирования для оптимизации. Gcc намеренно отмечает его как холодный и, таким образом, отключает для него некоторые оптимизации. Я не знаю, так ли это здесь, но это важная привычка.bitmap
это глобальный. Версия без CSEd использует операнд памяти дляcmp
, что не является проблемой для perf в этом случае. Если бы он был локальным, компилятор мог бы предположить, что другие указатели не могут «знать о нем», и указывать на него. Неплохо хранить выражения, включающие глобальные переменные, во временных переменных, если это улучшает (или не ухудшает) читаемость или если производительность критична. Если ничего не происходит, такие местные жители обычно живут в регистрах, и их никогда не проливают.На самом деле в вашем фрагменте кода недостаточно информации, чтобы сказать, и единственная вещь, о которой я могу думать, - это алиасинг. С нашей точки зрения, это довольно очевидно , что вы не хотите ,
p
иbitmap
к точке в том же месте в памяти, но компилятор не знает , что и (потому чтоp
это типаchar*
), компилятор должен сделать этот код работать , даже еслиp
иbitmap
перекрытие.В данном случае это означает, что если цикл изменяется
bitmap->width
через указатель,p
то это нужно будет увидеть при повторном чтенииbitmap->width
позже, что, в свою очередь, означает, что сохранение его в локальной переменной будет незаконным.При этом я считаю, что некоторые компиляторы на самом деле иногда генерируют две версии одного и того же кода (я видел косвенные доказательства этого, но никогда напрямую не искал информацию о том, что делает компилятор в этом случае), и быстро проверяет, соответствуют ли указатели псевдоним и запустите более быстрый код, если он определит, что это нормально.
При этом я поддерживаю свой комментарий о простом измерении производительности двух версий, мои деньги заключаются в том, что я не вижу стабильной разницы в производительности между двумя версиями кода.
На мой взгляд, подобные вопросы допустимы, если ваша цель - узнать о теориях и методах оптимизации компилятора, но это пустая трата времени (бесполезная микрооптимизация), если ваша конечная цель - ускорить работу программы.
источник
restrict
квалификатор не был бы ответом на проблему сглаживания в этом случае?restrict
в основном это случайные ошибки. MSVC - единственный компилятор, который я видел, который, кажется, делает это правильно. ICC теряет информацию о псевдонимах через вызовы функций, даже если они встроены. И GCC обычно не может получить никаких преимуществ, если вы не объявите каждый входной параметр какrestrict
(в том числеthis
для функций-членов).char
псевдонимы всех типов, поэтому, если у вас есть char *, вы должны использовать егоrestrict
для всего. Или, если вы отключили строгие правила псевдонима GCC,-fno-strict-aliasing
тогда все будет считаться возможным псевдонимом.restrict
-подобной семантики в C ++ является N4150 .Хорошо, ребята, я измерил
GCC -O3
(используя GCC 4.9 в Linux x64).Оказывается, вторая версия работает на 54% быстрее!
Так что, думаю, дело в алиасинге, я об этом не думал.
[Редактировать]
Я снова попробовал первую версию со всеми указателями, определенными с помощью
__restrict__
, и результаты такие же. Странно ... Либо сглаживание не проблема, либо по какой-то причине компилятор плохо его оптимизирует даже с__restrict__
.[Изменить 2]
Хорошо, думаю, мне удалось в значительной степени доказать, что проблема в псевдониме. Я повторил свой первоначальный тест, на этот раз используя массив, а не указатель:
И измерил (пришлось использовать "-mcmodel = large", чтобы связать его). Потом попробовал:
Результаты измерений были такими же - похоже, компилятор смог оптимизировать его самостоятельно.
Затем я попробовал исходные коды (с указателем
p
), на этот разp
типаstd::uint16_t*
. И снова результат был таким же - за счет строгого алиасинга. Затем я попытался построить с помощью «-fno-strict-aliasing» и снова увидел разницу во времени.источник
В других ответах указывалось, что подъем операции указателя из цикла может изменить определенное поведение из-за правил псевдонима, которые позволяют char использовать псевдоним что-либо и, следовательно, не является допустимой оптимизацией для компилятора, хотя в большинстве случаев это очевидно правильно для человека программист.
Они также указали, что вывод операции из цикла обычно, но не всегда, является улучшением с точки зрения производительности и часто является отрицательным с точки зрения удобочитаемости.
Хочу отметить, что часто бывает «третий путь». Вместо того, чтобы считать нужное вам количество итераций, вы можете отсчитывать до нуля. Это означает, что количество итераций необходимо только один раз в начале цикла, после этого его не нужно сохранять. Еще лучше на уровне ассемблера, это часто устраняет необходимость в явном сравнении, поскольку операция декремента обычно устанавливает флаги, которые указывают, был ли счетчик нулевым как до (флаг переноса), так и после (нулевой флаг) декремента.
Обратите внимание, что эта версия цикла дает значения x в диапазоне 1..width, а не в диапазоне 0 .. (width-1). В вашем случае это не имеет значения, потому что вы на самом деле ни для чего не используете x, но об этом нужно знать. Если вам нужен цикл обратного отсчета со значениями x в диапазоне 0 .. (ширина-1), вы можете это сделать.
Вы также можете избавиться от приведений в приведенных выше примерах, если хотите, не беспокоясь о его влиянии на правила сравнения, поскольку все, что вы делаете с bitmap-> width, - это присваивает его напрямую переменной.
источник
x --> 0
, что приводит к оператору «даунто». Очень забавно. PS Я не считаю, что создание переменной для конечного условия отрицательно для читабельности, на самом деле может быть наоборот.static_cast<unsigned>(bitmap->width)
и использованиеwidth
вместо этого в цикле на самом деле улучшает удобочитаемость, потому что теперь читателю становится меньше вещей, которые нужно анализировать на каждую строку. Однако мнения других могут отличаться.do { } while()
, потому что в ASM вы создаете циклы с условной ветвью в конце. Обычнаяfor(){}
иwhile(){}
петля требует дополнительных инструкций , чтобы проверить условие цикла один раз перед циклом, если компилятор не может доказать это всегда работает по крайней мере один раз. В любом случае используйтеfor()
или,while()
когда полезно проверить, должен ли цикл запускаться один раз, или когда он более читабелен.Единственное, что может помешать оптимизации, - это строгое правило сглаживания . Вкратце :
Исключение относится также к
unsigned
иsigned
char
указатели.Так обстоит дело с вашим кодом: вы изменяете,
*p
черезp
какой являетсяunsigned char*
, поэтому компилятор должен предположить, что он может указывать наbitmap->width
. Следовательно, кешированиеbitmap->width
является недопустимой оптимизацией. Это поведение предотвращения оптимизации показано в ответе YSC .Если и только если бы он
p
указывал на не- типchar
и не-decltype(bitmap->width)
тип, кеширование могло бы быть возможной оптимизацией.источник
Первоначально заданный вопрос:
И мой ответ на этот вопрос (я получил хорошее сочетание голосов как за, так и против ...)
Несмотря на отрицательные голоса (и теперь вижу проблему с псевдонимом), я все еще доволен этим как правильным ответом. Если вы не знаете, стоит ли что-то оптимизировать, вероятно, нет.
Конечно, совсем другой вопрос:
Во-первых, нужно ли вашему приложению или библиотеке работать быстрее, чем сейчас? Пользователь слишком долго ждал? Прогнозирует ли ваше программное обеспечение вчерашнюю погоду вместо завтрашней?
Только вы можете действительно сказать это, основываясь на том, для чего предназначено ваше программное обеспечение и чего ожидают ваши пользователи.
Предполагая, что ваше программное обеспечение нуждается в некоторой оптимизации, следующее, что нужно сделать, - это начать измерения. Профилировщики скажут вам, где ваш код тратит время. Если ваш фрагмент не является узким местом, его лучше оставить в покое. Профилировщики и другие инструменты измерения также сообщат вам, изменили ли ваши изменения ситуацию. Можно часами пытаться оптимизировать код только для того, чтобы обнаружить, что вы не сделали заметной разницы.
Если вы не пишете «оптимизированный» код, ваш код должен быть настолько ясным, чистым и кратким, насколько вы можете его сделать. Аргумент «Преждевременная оптимизация - зло» не является оправданием небрежного или неэффективного кода.
Оптимизированный код обычно жертвует некоторыми из вышеперечисленных атрибутов ради производительности. Это может включать в себя введение дополнительных локальных переменных, наличие объектов с более широкой, чем ожидалось, областью видимости или даже изменение обычного порядка цикла. Все это может быть менее ясным или кратким, поэтому задокументируйте код (кратко!) О том, почему вы это делаете.
Но часто с «медленным» кодом эти микрооптимизации становятся последним средством. В первую очередь следует изучить алгоритмы и структуры данных. Есть ли способ вообще избежать работы? Можно ли заменить линейный поиск двоичным? Будет ли связанный список здесь быстрее, чем вектор? Или хеш-таблица? Могу ли я кэшировать результаты? Принятие здесь хороших «эффективных» решений часто может повлиять на производительность на порядок или больше!
источник
В такой ситуации я использую следующий шаблон. Он почти такой же короткий, как ваш первый случай, и лучше, чем второй случай, потому что он сохраняет временную переменную локальной для цикла.
Это будет быстрее с менее умным компилятором, отладочной сборкой или некоторыми флагами компиляции.
Edit1 : Размещение постоянной операции вне цикла - хороший шаблон программирования. Показывает понимание основ работы с машиной, особенно на C / C ++. Я бы сказал, что попытки проявить себя должны быть на людях, которые не следуют этой практике. Если компилятор наказывает за хороший шаблон, это ошибка в компиляторе.
Edit2:: Я сравнил свое предложение с исходным кодом на vs2013, получил улучшение% 1. Можем ли мы сделать лучше? Простая ручная оптимизация дает улучшение в 3 раза по сравнению с исходным циклом на машине x64, не прибегая к экзотическим инструкциям. В приведенном ниже коде используется система с прямым порядком байтов и правильно выровненное растровое изображение. ТЕСТ 0 - оригинальный (9 секунд), ТЕСТ 1 - быстрее (3 секунды). Бьюсь об заклад, кто-нибудь может сделать это еще быстрее, и результат теста будет зависеть от размера растрового изображения. Определенно скоро в будущем компилятор сможет производить стабильно самый быстрый код. Боюсь, это будет будущее, когда компилятор будет также программистом ИИ, поэтому мы останемся без работы. Но пока просто напишите код, который показывает, что вы знаете, что дополнительная операция в цикле не требуется.
источник
Следует учитывать две вещи.
А) Как часто будет выполняться оптимизация?
Если ответ выдается не очень часто, например, только когда пользователь нажимает кнопку, не беспокойтесь, если это сделает ваш код нечитаемым. Если ответ - 1000 раз в секунду, вы, вероятно, захотите продолжить оптимизацию. Если это даже немного сложно, не забудьте добавить комментарий, чтобы объяснить, что происходит, чтобы помочь следующему парню, который придет.
Б) Усложнит ли это исправление / устранение неполадок кода?
Если вы не видите значительного увеличения производительности, то делать ваш код загадочным просто для экономии нескольких тактов - не лучшая идея. Многие люди скажут вам, что любой хороший программист должен уметь смотреть на код и понимать, что происходит. Это верно. Проблема в том, что в деловом мире дополнительное время на выяснение этого стоит денег. Итак, если вы можете сделать его приятнее для чтения, сделайте это. Ваши друзья будут вам за это благодарны.
Тем не менее, я лично использую пример B.
источник
Компилятор умеет многое оптимизировать. В вашем примере вы должны выбрать удобочитаемость, удобство обслуживания и то, что соответствует стандарту вашего кода. Дополнительные сведения о том, что можно оптимизировать (с помощью GCC), см. В этом сообщении в блоге .
источник
Как правило, позволяйте компилятору выполнять оптимизацию за вас, пока вы не решите, что вам следует взять на себя ответственность. Логика этого не имеет ничего общего с производительностью, а скорее с удобочитаемостью. В подавляющем большинстве случаев читабельность вашей программы важнее ее производительности. Вы должны стремиться писать код, который будет легче читать человеку, и беспокоиться об оптимизации только тогда, когда вы уверены, что производительность важнее, чем ремонтопригодность вашего кода.
Как только вы увидите, что производительность имеет значение, вам следует запустить профилировщик кода, чтобы определить, какие циклы неэффективны, и оптимизировать их по отдельности. Действительно, могут быть случаи, когда вы захотите провести эту оптимизацию (особенно если вы переходите на C ++, где задействованы контейнеры STL), но затраты с точки зрения удобочитаемости велики.
Вдобавок я могу думать о патологических ситуациях, когда это могло действительно замедлить код. Например, рассмотрим случай, когда компилятор не смог доказать, что это
bitmap->width
было постоянным в процессе. Добавляяwidth
переменную, вы заставляете компилятор поддерживать локальную переменную в этой области. Если по какой-то причине, зависящей от платформы, эта дополнительная переменная помешала некоторой оптимизации пространства стека, возможно, придется реорганизовать то, как она испускает байт-коды, и создать что-то менее эффективное.Например, в Windows x64 необходимо вызвать специальный вызов API
__chkstk
в преамбуле функции, если функция будет использовать более 1 страницы локальных переменных. Эта функция дает окнам возможность управлять страницами защиты, которые они используют для расширения стека при необходимости. Если ваша дополнительная переменная увеличивает использование стека с 1 страницы до 1 или выше, ваша функция теперь обязана вызывать__chkstk
каждый раз, когда она вводится. Если бы вы оптимизировали этот цикл на медленном пути, вы могли бы замедлить быстрый путь вниз больше, чем вы сэкономили на медленном пути!Конечно, это немного патологично, но суть этого примера в том, что вы действительно можете замедлить работу компилятора. Это просто показывает, что вам нужно профилировать свою работу, чтобы определить, куда идет оптимизация. А пока, пожалуйста, не жертвуйте удобочитаемостью ради оптимизации, которая может иметь значение, а может и не иметь.
источник
Сравнение неверно , так как два фрагмента кода
и
не эквивалентны
В первом случае
width
является зависимым, а не константным, и нельзя предполагать, что он может не измениться между последующими итерациями. Таким образом, его нельзя оптимизировать, но его необходимо проверять в каждом цикле .В вашем оптимизированном случае локальной переменной
bitmap->width
в какой-то момент во время выполнения программы присваивается значение . Компилятор может проверить, что на самом деле это не изменилось.Вы думали о многопоточности, или, может быть, значение может быть внешне зависимым, так что его значение непостоянно. Как можно ожидать, что компилятор все это выяснит, если вы не скажете?
Компилятор может работать настолько хорошо, насколько позволяет ваш код.
источник
Если вы не знаете, как именно компилятор оптимизирует код, лучше проводить оптимизацию самостоятельно, сохраняя читабельность кода и дизайн. Практически сложно проверить ассемблерный код каждой функции, которую мы пишем для новых версий компилятора.
источник
Компилятор не может оптимизировать,
bitmap->width
потому что значениеwidth
может быть изменено между итерациями. Есть несколько наиболее частых причин:iterator::end()
илиcontainer::size()
так что трудно предсказать , если он будет всегда возвращает тот же результат.Подводя итог (мое личное мнение) для мест, где требуется высокий уровень оптимизации, вам нужно сделать это самостоятельно, в других местах просто оставьте это, компилятор может оптимизировать его или нет, если нет большой разницы в читаемости кода, является основной целью.
источник