+7 (495) 229-0436   shopadmin@itshop.ru 119334, г. Москва, ул. Бардина, д. 4, корп. 3
 
 
Вход
 
 
Каталог
 
 
Подписка на новости
Новости ITShop
Windows 7 и Office: Новости и советы
Обучение и сертификация Microsoft
Вопросы и ответы по MSSQLServer
Delphi - проблемы и решения
Adobe Photoshop: алхимия дизайна
 
Ваш отзыв
Оцените качество магазина ITShop.ru на Яндекс.Маркете. Если вам нравится наш магазин - скажите об этом Google!
 
 
Способы оплаты
 
Курс расчета
 
 1 у.е. = 86.57 руб.
 
 Цены показывать:
 
 
 
 
  
Новости, статьи, акции
 

Как заставить компилятор C/C++ генерировать плохой код

06.12.2012 17:10
m1kc

Это перевод статьи "How to trick C/C++ compilers into generating terrible code?",

автор оригинала - Aater Suleman.

На курсе архитектуры ЭВМ мне сказали, что процессор похож на машину. Руль и педали - это ISA, двигатель - микроархитектура, а программа - водитель. Продолжая эту аналогию, скажу, что использование компьютера похоже на управление машиной через пульт дистанционного управления. Пульт - это клёвая вещь, но в то же время важно понимать, как он работает. Даже в профессиональном ПО я видел много примеров кода, который может смутить даже самый умный компилятор. В этой статье я расскажу об основных методах запутывания компиляторов.

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

Компиляторы

Компилятор берёт ваш код на языке высокого уровня и преобразует его в код машинный. Схема работы компилятора выглядит примерно так:

  1. Выполнить синтаксический анализ кода и построить его промежуточное представление.
  2. Оптимизировать промежуточное представление.
  3. Сгенерировать машинный код.
  4. Выполнить линковку (компоновку).

Оптимизация кода требует от компилятора выполнения довольно объёмного анализа. Простой пример - удаление мёртвого кода (англ. dead code elimination, DCE). При выполнении DCE компилятор исключает из программы код, исполнение которого не влияет на результат работы программы. Например, цикл в строках 3 и 4 будет исключён компилятором, потому что результат выполнения цикла не используется нигде.

1: void foo(){
2:   int x;
3:   for(int i = 0; i < N; i++)
4:     x += i;
5: }

При выполнении DCE компилятор производит обратный анализ потока данных (def-use), чтобы определить, какие из переменных не используются. Затем он удаляет строки, которые выполняют запись в эти неиспользуемые переменные. Чтобы компилятор смог выполнить DCE и другие подобные оптимизации, у него должна быть возможность узнать со 100%-ной уверенностью, будет ли выполняться доступ к переменной. Здесь и открываются возможности для запутывания компилятора. Мы можем использовать несколько хорошо известных "узких мест".

Вызовы функций

Анализ, выполняемый компилятором, - очень сложная вычислительная задача. Для справки - код самого компилятора, вероятно, является самым сложным кодом в мире. Более того, проблемы оптимизации проявляются очень быстро. Чтобы удержать время компиляции в разумных рамках, компиляторы часто ограничивают область анализа функциями и выполняют ограниченный межпроцедурный анализ (англ. inter-procedural analysis, IPA) или не выполняют его вовсе. Вдобавок к этому компилятор, воспринимающий функции как независимые сущности, может компилировать несколько файлов одновременно.

Добавление нескольких слоёв вызовов функций, в идеале ещё и разбросанных по разным файлам, часто является хорошим способом запутать компилятор.

Давайте посмотрим на этот пример:

void foo(){
  int x;
  for(int i = 0; i < N; i++)
    x += bar(i);
}

И в другом файле:

int bar(int i){
  return i;
}

Программист может подумать, что лишние ресурсы здесь тратятся только на вызовы bar(). Однако во время компиляции foo() компилятор не может увидеть, что у bar() нет никаких побочных эффектов, например, записи в глобальную переменную или операций ввода-вывода. Поэтому он отключает DCE, и бесполезный цикл теперь тоже будет выполняться. Результатом может стать существенное падение производительности.

Указатели

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

Указатель, теоретически, может указывать на любое место в памяти. Как только компилятор встречает в коде указатель, он больше не может быть уверен, к каким переменным будет осуществляться доступ. Следовательно, он предполагает, что к нескольким переменным в области действия может быть осуществлён доступ через указатель. В глазах компилятора это часто создаёт кучу ложных зависимостей. Мой классический пример - вот этот цикл (я нашёл его в программе для обработки изображений):

for(int i = 0; i < N; i++)
 *a++ = *b++ + *c++;

После внимательного изучения человек может понять, что код на самом деле просто делает следующее:

for(int i = 0; i < N; i++)
 a[i] = b[i] + c[i];
a += N;
b += N;
c += N;

Первый код запутывает компилятор. Даже если итерации цикла независимы, компилятор не считает их таковыми; по этой причине он не может векторизовать данный кусок кода. А вот второй - может.

Для справки: векторизованный код выполняется в 5 раз быстрее. Использование многопоточной обработки может дать прирост в 4 раза на 4 ядрах с небольшими усилиями со стороны программиста. Этот же код, автоматически векторизованный, даёт прирост скорости в 5 раз без каких-либо усилий. Я пробовал Intel C Compiler (ICC) 9.1 и GCC 4.1. Результаты приведены для ICC.

Если идей больше нет, используйте глобальные переменные

Глобальные переменные считаются злом по многим причинам. Я приведу ещё одну. Поскольку у них глобальная область видимости, код, использующий глобальные переменные, не может быть полностью оптимизирован компилятором. Например, я заметил, что следующий код выполняется на 30% быстрее, если переменная N локальная, а не глобальная.

for(int i = 0; i < N; i++)
 a[i] = b[i] + c[i];

Когда вы объявляете переменную N глобальной, компилятор оставляет её в памяти и добавляет в цикл инструкцию для её загрузки. Если же объявить Nлокальной, компилятор загружает её в регистр. Можно ругать такое поведение компилятора (потому что N не является volatile), но приходится работать с тем, что есть.

Заключение

Важно помнить об IPA и указателях, когда вы пишете код на C/C++. Так вы сможете помочь компилятору сгенерировать код, который будет выполняться быстрее.

Кстати: я заметил, что и GCC, и ICC чувствительны к порядку линковки, т.е. сгенерированный код зависит от порядка, в котором указаны входные файлы в командной строке компоновщика. Это может оказать сильное влияние (до 10%) на производительность из-за кэширования и предсказания ветвлений. Если вы по-настоящему цените производительность, стоит попробовать поиграться и с порядком линковки.

Ссылки по теме

  
Помощь
Задать вопрос
 программы
 обучение
 экзамены
 компьютеры
Бесплатный звонок
ICQ-консультанты
Skype-консультанты

Общая справка
Как оформить заказ
Тарифы доставки
Способы оплаты
Прайс-лист
Карта сайта
 
Бестселлеры
Курсы обучения "Atlassian JIRA - система управления проектами и задачами на предприятии"
Microsoft Windows 10 Профессиональная 32-bit/64-bit. Все языки. Электронный ключ
Microsoft Office для Дома и Учебы 2019. Все языки. Электронный ключ
Курс "Oracle. Программирование на SQL и PL/SQL"
Курс "Основы TOGAF® 9"
Microsoft Office 365 Персональный 32-bit/x64. 1 ПК/MAC + 1 Планшет + 1 Телефон. Все языки. Подписка на 1 год. Электронный ключ
Курс "Нотация BPMN 2.0. Ее использование для моделирования бизнес-процессов и их регламентации"
 

О нас
Интернет-магазин ITShop.ru предлагает широкий спектр услуг информационных технологий и ПО.

На протяжении многих лет интернет-магазин предлагает товары и услуги, ориентированные на бизнес-пользователей и специалистов по информационным технологиям.

Хорошие отзывы постоянных клиентов и высокий уровень специалистов позволяет получить наивысший результат при совместной работе.

В нашем магазине вы можете приобрести лицензионное ПО выбрав необходимое из широкого спектра и ассортимента по самым доступным ценам. Наши менеджеры любезно помогут определиться с выбором ПО, которое необходимо именно вам. Также мы проводим учебные курсы. Мы приглашаем к сотрудничеству учебные центры, организаторов семинаров и бизнес-тренингов, преподавателей. Сфера сотрудничества - продвижение бизнес-тренингов и курсов обучения по информационным технологиям.



 

О нас

 
Главная
Каталог
Новинки
Акции
Вакансии
 

Помощь

 
Общая справка
Как оформить заказ
Тарифы доставки
Способы оплаты
Прайс-лист
Карта сайта
 

Способы оплаты

 

Проекты Interface Ltd.

 
Interface.ru   ITShop.ru   Interface.ru/training   Olap.ru   ITnews.ru  
 

119334, г. Москва, ул. Бардина, д. 4, корп. 3
+7 (495) 229-0436   shopadmin@itshop.ru
Проверить аттестат
© ООО "Interface Ltd."
Продаем программное обеспечение с 1990 года