#c #compiler-optimization
#c #оптимизация компилятора
Вопрос:
Исходя в основном из опыта работы с Python, сейчас я изучаю как C, так и сборку x86-64. Ранее я использовал C косвенно через Cython, но теперь я изучаю собственно C в дополнение к сборке.
Мой основной вопрос заключается в том, какое мышление я должен поставить себе, когда дело доходит до оптимизации компиляторов. Должен ли я просто позволить компилятору выполнять свою работу, но, как только я достаточно разбираюсь в сборке, начать проверять и подтверждать вывод сборки? Это то, что делают ответственные программисты на C, желающие писать высокопроизводительный код?
Вопрос был вызван тем, что я хотел проверить, на что gcc 7.5.0
можно оптимизировать приведенный ниже код. В частности, я побежал objdump
, чтобы выяснить, как будет оптимизирован доступ к массиву дважды с одним и тем же индексом на разных уровнях.
-O3
Были некоторые инструкции, которые я еще не изучил, напримерmovaps XMMWORD PTR [rsp 0x10],xmm0
- Уровни
-O2
и-O1
были несколько понятнее, но все же я не до конца понял это - На уровне
-O0
, я полагаю, я мог видеть довольно простой перевод кода, к которому, я думаюmessages[idx]
, действительно обращались дважды
Мой вопрос не в том, когда следует использовать эти уровни. Я просто спрашиваю более опытных программистов, если это то, что вы делаете, запускаете код с высокой оптимизацией и проверяете выходные данные сборки, чтобы убедиться, что все так, как ожидалось? Это естественный рабочий процесс для людей, которые хотят действительно знать, какой машинный код создает компилятор?
Я понимаю, что приведенный ниже пример — это тривиальная возможность для оптимизации, но вы только что узнали, что определенные оптимизации происходят наверняка, и вы больше не думаете о них? Существует не так много информации о том, какие преобразования и оптимизации могут иметь место, не говоря уже о том факте, что компиляторы не оставляют заметок или сообщений для программистов, чтобы понять, что было оптимизировано и почему, поэтому я просто не могу представить себе другого способа, кроме простого изучения всего этого на практике. Спасибо.
#include <stddef.h>
#include <stdio.h>
int main(int argc, char ** argv)
{
size_t len_messages = 9;
int messages[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
for(size_t idx=0; idx < len_messages; idx ) {
printf("Accessing here %d and there %dn", messages[idx], messages[idx]);
}
return 0;
}
Комментарии:
1. Я думаю, что это зависит от среды, в которой вы работаете. Я вообще не думаю об оптимизации — я просто говорю
-O3
и позволяю компилятору делать свое дело — если только не кажется, что есть проблема. И в моей области это редко бывает — компилятор обычно генерирует довольно хороший код. Я подозреваю, что во многих областях вам придется быть намного более активным. Честно говоря, я подозреваю, что вы получите мнения по этому поводу, но не исчерпывающие ответы.2. Я редко смотрю на ассемблерный код, хотя меня очень волнует оптимизация. Далеко не просто посмотреть на два куска ассемблерного кода и сказать: это быстрее, чем это — современные процессоры очень сложны. Более того, ключевым фактором, определяющим производительность, является то, насколько хорошо программа работает с системой памяти (все эти кэши!), И это часто легче увидеть с более высокого уровня. Для меня, и я подозреваю, что многие другие, время оптимизации тратится на просмотр результатов профилировщика и экспериментирование с «алгоритмами» более высокого уровня
3. Вместо того, чтобы вручную проверять сборку, вы должны сначала запустить свой код через профилировщик. Ищите горячие точки, затем сначала сосредоточьтесь на алгоритмической сложности, согласованности кэша и т. Д. В этих областях. Только после того, как вы уверены, что ваш дизайн оптимален, вы должны посмотреть на сборку (если это все еще необходимо на данный момент).
4. Только безумные (или неудачливые) люди смотрят на сборку для всего кода языка высокого уровня, который они пишут.
5. Прочитайте этот проект отчета . Более десятка страниц, связанных с вашим вопросом
Ответ №1:
Мой основной вопрос заключается в том, какое мышление я должен поставить себе, когда дело доходит до оптимизации компиляторов. Должен ли я просто позволить компилятору выполнять свою работу, но, как только я достаточно разбираюсь в сборке, начать проверять и подтверждать вывод сборки?
В основном нет.
Разные фрагменты кода влияют на производительность в разной степени — фрагмент кода, который используется только один раз во время инициализации, не сильно повлияет на производительность, а фрагмент кода в середине цикла, который часто выполняется, может сильно повлиять на производительность. Оптимизация с помощью сборки требует времени разработчика и переносимости; и часто эти дополнительные затраты не могут быть оправданы незначительными улучшениями производительности кода, который выполняется не часто.
По этой причине основная тактика заключается в использовании профилировщика, чтобы определить, где находятся наиболее важные (для производительности) фрагменты кода; и исследовать улучшения производительности только для этих фрагментов.
Однако «исследовать улучшения производительности» по-прежнему не обязательно означает переход непосредственно к сборке. Вы думаете об улучшении алгоритма, улучшении структур данных и локальности кэша, улучшении параллелизма («больше потоков!») И т. Д.
После всего этого вы можете посмотреть на сборку, которую генерирует компилятор, и посмотреть, сможете ли вы найти способ улучшить / оптимизировать ее вручную. Вы также можете этого не делать.
Причина, по которой вы все еще можете не использовать язык ассемблера, заключается в том, что разные процессоры разные. Вы можете оптимизировать для одного процессора (независимо от того, что есть на вашем компьютере) и значительно замедлить работу программного обеспечения на других процессорах (независимо от того, что есть у конечных пользователей, которые запускают ваше программное обеспечение); или вы можете полагаться на функции (например, AVX512), которые могут не существовать. Конечно, это также означает, что результаты, полученные вами в результате профилирования, не так полезны, как вы могли бы подумать (достаточно хороши для грубой оценки и никогда не могут использоваться в качестве точного представления, применимого ко всем процессорам).
Чтобы обойти это, вам может понадобиться несколько разных версий на языке ассемблера для разных процессоров — одна для «64-разрядной Intel с AVX-512», одна для «64-разрядной Intel с AVX2», одна для «64-разрядной Intel без AVX», еще 2 версии для AMD, потому что выобнаружил, что несколько инструкций занимают больше времени на AMD, а несколько других инструкций выполняются быстрее на AMD; затем еще один набор разных версий для 64-разрядной ARM, затем PowerPC, затем…
В принципе; оптимизация в сборке встречается редко. Для «сильно загруженной» библиотеки (например, декодера MPEG, библиотеки больших чисел, …) это может иметь большой смысл, и для нескольких критически важных частей большой программы это может быть оправдано; но помимо этого, вероятно, у вас есть гораздо более важные дела с вашимвремя.
Комментарии:
1. Это интересный ответ, и я согласен с вами, но вопрос был скорее в духе «откуда вы знаете, что компилятор оптимизирует вещи так, как вы ожидаете, учитывая, что это происходит беззвучно», а не «когда оптимизировать в сборке». Я думаю, что вы начали отвечать с первой точки зрения, но потом все равно закончили со второй 🙂 Если бы вы могли, могли бы просто добавить примечание о том, что, по вашему опыту, если только низкоуровневый код не является математически сложным или, возможно, если вы не пишете компилятор самостоятельно, редко можно проверить, что делает современный компилятор?
2. @Terry: Это не так, как все это работает.. Если вы включаете оптимизацию («-O3»), вы знаете, что компилятор старался изо всех сил, и вы знаете, что лучшее в компиляторе может быть «хуже идеального», и вам просто все равно (и знаете, что лучшее в компиляторе может быть лучше или хуже, чем то, чего вы ожидаливремя). Если вы не включаете оптимизацию, то вы знаете, что компилятор не пытался (и можете ожидать, что результат будет ужасным).
3. @Terry: Обратите внимание, что это можно считать «делегированием» — вы делегируете ответственность за оптимизацию компилятору (и разработчикам компилятора), чтобы вы могли сказать: «LOL, это больше не моя проблема!».
4. Хотел бы я принять два ответа — в конце концов я принял ответ от @rurban, потому что он познакомил меня с новым инструментом на этом пути. Еще раз спасибо, Брендан, ваш ответ тоже был очень полезен.
Ответ №2:
Я редко смотрю на дизассемблирование в одиночку. В основном я декомпилирую функцию с помощью Ghidra, чтобы посмотреть, что происходит с оптимизатором. Тогда вы получаете гораздо большую и лучшую картину. На более знакомом языке, где вы все еще можете видеть сгенерированную сборку.