Методики оценки качества машинной оптимизации
Задача оценки качества кодогенерации намного сложнее, чем может показаться на первый взгляд. Прежде всего, следует разделять, собственно, сам компилятор и его окружение (среду, библиотеки и т.д.). В частности совершенно некорректно сравнивать размер откомпилированного примера "Hello, World" с его ассемблерной реализацией. Вызов 'printf("xxx");' компилятор транслирует приблизительно в следующий код: "push offset xxx\call printf\pop eax" – круче уже не оптимизируешь!
Обратите внимание на размер объективного файла, сформированного компилятором. Не правда ли, он мало в чем не уступает объективному файлу ассемблерной реализации? Конечно, после подключения всех необходимых библиотек, размер откомпилированного файла увеличивается в десятки раз, в то время как объем ассемблерного модуля практически не изменяется. Да, это так, но при чем здесь компилятор?! Ему встретился вызов printf, – он и включил его в объективный файл. Если бы программист захотел вывести строку напрямую, – через соответствующую API функцию операционной системы (как он поступил в ассемблерной реализации), исполняемый файл сразу бы похудел на десяток-другой килобайт. Что еще остается? Ах да, среда, называемая так же RTL (Run Time Library
– библиотека времени исполнения), – служебные функции, вызываемые самим компилятором. Несмотря на то, что библиотеки времени исполнения являются неотъемлемым компонентом компилятора, к качеству кодогенерации они не имеют никакого отношения, т.к. с точки зрения компилятора функции RTL ничем не отличаются от обычных библиотечных функций.
Избыточность штатных библиотечных функций и библиотеки времени исполнения на крохотных проектах очевидна, – вывод строки "Hello, World" не использует и сотой доли возможностей функции printf, но в программе, состоящей из нескольких тысяч строк, соотношение между полезными и служебными функциями нормализуется и коэффициент полезного действия библиотек практически вплотную приближается к единице.
Таким образом, сравнивать эффективность компилятора и ассемблера на примере библиотечных функций – это вопиющая некорректность. Следует рассматривать лишь чистые реализации, не обращающиеся к внешнему коду. В противном случае, будет сравниваться не качество машинной и ручной оптимизации, а совершенство библиотек (написанных, кстати, в большинстве своем на ассемблере) с ручной оптимизацией. Совершенно бесполезно сравнивать и размеры объективных файлов, – помимо кода они содержат массу посторонней информации, причем в рассматриваемых ниже примерах ее объем превышает размер машинного кода в десятки раз!
Истинную картину вещей дает лишь дизассемблер, – загружаем в него объективный или исполняемый – без разницы – файл и от адреса конца функции вычитаем адрес ее начала. Полученная разность и будет подлинным размером исследуемого кода.
Измерять производительность еще проще – достаточно засечь время выполнения функции и… Правда, тут есть одно "но". Если уж мы взялись оценивать именно качество кодогенерации, а не быстродействие компьютера, следует учесть, и по возможности свести к нулю, все посторонние эффекты. Во-первых, к моменту вызова функции все, обрабатываемые ей данные, должны целиком находиться в кэше первого уровня, иначе неповоротливость памяти сотрет все различия в производительности тестируемого кода. Во-вторых, размер обрабатываемых данных должен быть достаточно велик для того, чтобы замаскировать накладные расходы на вызов функции, передачу аргументов, снятие показаний со счетчика производительности и т.д. Все нижеследующие примеры обрабатывают 4.000 элементов типа int, – это дает стабильный и хорошо воспроизводимый результат, т.к. "насыщение" наступает уже на 1.000 элементах, после чего накладные расходы уже не играют сколь ни будь заметной роли.