Эффективные стратегии оптимизации относительно современных компиляторов C++

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

Это известно, что некоторая оптимизация, например, развертывание цикла, обрабатывается в эти дни намного эффективнее компилятором, чем программистом, вмешивающимся вручную. Какие методы все еще стоят? Очевидно, я выполню все, что я пробую через профилировщика, но если существует расхожее мнение относительно того, что имеет тенденцию работать и что не делает, оно сэкономило бы мне значительное время.

Я знаю, что оптимизация является очень компилятором - и архитектурно-зависимый. Я использую компилятор C++ Intel, предназначающийся для Core 2 Duo, но я также интересуюсь какой работы хорошо для gcc, или для "любого современного компилятора".

Вот некоторые конкретные идеи, которые я рассматриваю:

  • Там какое-либо преимущество к замене контейнеров/алгоритмов STL со скрученными вручную? В частности, моя программа включает очень многочисленную приоритетную очередь (в настоящее время a std::priority_queue) чье управление занимает много общего времени. Это - что-то стоящее изучить, или реализация STL уже вероятна самое быстрое?
  • Вдоль подобных строк, для std::vectors, чьи необходимые размеры неизвестны, но имеют довольно небольшую верхнюю границу, действительно ли является прибыльным заменить их статически выделенными массивами?
  • Я нашел, что динамическое выделение памяти часто является серьезным узким местом, и что устранение его может привести к значительным ускорениям. Как следствие я интересен в компромиссах производительности возврата больших временных структур данных значением по сравнению с возвратом указателем по сравнению с передачей результата в ссылкой. Существует ли способ надежно определить, будет ли компилятор использовать RVO для данного метода (предполагающий, что вызывающая сторона не должна изменять результат, конечно)?
  • Как осведомленный о кэше компиляторы имеют тенденцию быть? Например, действительно ли стоит изучить переупорядочение вложенных циклов?
  • Учитывая научную природу программы, числа с плавающей запятой используются везде. Значительное узкое место в моем коде раньше было преобразованиями от плавающей точки до целых чисел: компилятор испустил бы код, чтобы сохранить текущий режим округления, изменить его, выполнить преобразование, затем восстановил бы старый режим округления---даже при том, что ничто в программе никогда не изменяло округляющийся режим! Отключение этого поведения значительно ускорило мой код. Есть ли какие-либо подобные связанные с плавающей точкой глюки, о которых я должен знать?
  • Одно последствие C++, скомпилированного и связанного отдельно, - то, что компилятор не может сделать то, что, казалось бы, было бы очень простой оптимизацией, такой как вызовы метода перемещения как strlen () из условий завершения цикла. Есть ли какая-либо оптимизация как этот, что я должен высматривать, потому что они не могут быть сделаны компилятором и должны быть сделаны вручную?
  • На обороте, там какие-либо методы, которых я должен избежать, потому что они, вероятно, вмешаются в способность компилятора автоматически оптимизировать код?

Наконец, для пресечения определенных видов в корне ответов:

  • Я понимаю, что оптимизация имеет стоимость с точки зрения сложности, надежности и пригодности для обслуживания. Для этого конкретного приложения увеличенная производительность стоит этих затрат.
  • Я понимаю, что лучшая оптимизация должна часто улучшать высокоуровневые алгоритмы, и это было уже сделано.
45
задан user168715 28 May 2010 в 21:15
поделиться

18 ответов

Есть ли какие-либо преимущества в замене контейнеров / алгоритмов STL на созданные вручную? В частности, моя программа включает в себя очень большую очередь приоритетов (в настоящее время std :: priority_queue), манипулирование которой занимает много общего времени. Стоит ли это изучить, или реализация STL уже, вероятно, самая быстрая из возможных?

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

Некоторые контейнеры (например, map , set , list ) полагаются на множество манипуляций с указателями. Хотя это противоречит здравому смыслу, это часто может привести к более быстрому коду, чтобы заменить их вектором . Результирующий алгоритм может быть от O (1) или O (log n) до O (n) , но из-за локальности кеша он может быть намного быстрее на практике. Профиль конечно.

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

Аналогичным образом, для std :: vectors, необходимые размеры которых неизвестны, но имеют достаточно небольшую верхнюю границу, выгодно ли заменять их статически распределенными массивами?

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

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

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

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

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

С другой стороны, есть ли какие-либо приемы, которых мне следует избегать, потому что они могут помешать способности компилятора автоматически оптимизировать код?

  • Используйте явное в ваших конструкторах с одним аргументом. Создание и разрушение временных объектов может быть скрыто в вашем коде.

  • Помните о вызовах конструкторов скрытых копий для больших объектов. В некоторых случаях подумайте о замене указателями.

  • Профиль, профиль, профиль. Настройте области, которые являются узкими местами.

26
ответ дан 26 November 2019 в 21:17
поделиться
  1. Использование пулов буферов памяти может иметь большое преимущество в производительности по сравнению с динамическим распределением. Тем более, если они уменьшают или предотвращают фрагментацию кучи при длительных запусках.

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

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

  4. Если очередь с одним приоритетом использует много времени в своей работе, может оказаться полезным создание батареи очередей, представляющих сегменты приоритета. В этом случае было бы торговать сложностью на скорость.

  5. Я заметил, что вы не упомянули об использовании инструкций типа SSE. Могут ли они быть применимы к вашему типу обработки чисел?

Желаем удачи.

7
ответ дан 26 November 2019 в 21:17
поделиться

Вот хорошая статья на эту тему.

5
ответ дан 26 November 2019 в 21:17
поделиться

Реализация очереди приоритетов STL довольно хорошо оптимизирована для того, что она делает, но определенные виды куч имеют особые свойства, которые могут улучшить вашу производительность на определенных алгоритмах. Кучи Фибоначчи - один из примеров. Кроме того, если вы храните объекты с небольшим ключом и большим объемом спутниковых данных, вы получите значительное улучшение производительности кеша, если сохраните эти данные отдельно, даже если это означает сохранение одного дополнительного указателя на каждый объект.

Что касается массивов, я обнаружил, что std :: vector даже немного превосходит массивы с константой времени компиляции. Тем не менее, его оптимизация носит общий характер, и конкретное знание шаблонов доступа вашего алгоритма может позволить вам дополнительно оптимизировать локальность кеша, выравнивание, окраску и т.д. -оптимизированные массивы могут в некоторых случаях сдвинуть этот порог размера проблемы в два раза, но вряд ли это будет иметь большое значение для небольших внутренних циклов, которые легко помещаются в кеш, или больших рабочих наборов, которые превышают размер любого Кеш процессора. Сначала поработайте с приоритетной очередью.

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

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

Что касается чисел с плавающей запятой, вам абсолютно необходимо использовать SSE. Это не обязательно требует самостоятельного изучения SSE, поскольку существует множество библиотек высокооптимизированного кода SSE, которые выполняют всевозможные важные научные вычислительные операции. Если вы компилируете 64-битный код, компилятор может автоматически выдать некоторый код SSE, поскольку SSE2 является частью набора инструкций x86_64. SSE также избавит вас от накладных расходов, связанных с плавающей запятой x87, поскольку он не преобразует внутренне туда и обратно в 80-битные значения. Эти преобразования также могут быть источником проблем с точностью, поскольку вы можете получить разные результаты от одного и того же набора операций в зависимости от того, как они компилируются, поэтому хорошо от них избавиться.

0
ответ дан 26 November 2019 в 21:17
поделиться

Есть ли какие-либо преимущества в замене контейнеров / алгоритмов STL на созданные вручную?

Как правило, если вы не работаете с плохой реализацией. Я бы не стал заменять контейнер или алгоритм STL только потому, что вы думаете, что можете написать более жесткий код. Я бы сделал это только в том случае, если версия STL более общая, чем это необходимо для вашей проблемы. Если вы можете написать более простую версию, которая делает именно то, что вам нужно, тогда, возможно, там будет некоторая скорость.

Я видел одно исключение - это замена std :: string копирования при записи на строку, не требующую синхронизации потоков.

для std :: vectors, необходимые размеры которых неизвестны, но имеют достаточно малую верхнюю границу, выгодно ли заменять их статически распределенными массивами?

Маловероятно. Но если вы тратите много времени на выделение до определенного размера, может быть выгодно добавить вызов reserve ().

компромисс производительности при возврате больших временных структур данных по значению, возврату по указателю и передаче результата по ссылке.

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

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

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

Существенным узким местом в моем коде было преобразование чисел с плавающей запятой в целые.

Ага. Недавно я обнаружил ту же проблему.

Одним из следствий раздельной компиляции и компоновки C ++ является то, что компилятор не может выполнять то, что казалось бы очень простой оптимизацией, например, перемещать вызовы методов, таких как strlen (), из условий завершения цикла.

Некоторые компиляторы могут с этим справиться. В Visual C ++ есть опция «генерации кода во время компоновки», которая эффективно повторно вызывает компилятор для дальнейшей оптимизации.И в случае таких функций, как strlen , многие компиляторы распознают это как внутреннюю функцию.

Есть ли какие-то оптимизации, подобные этой, на которые я должен обратить внимание, потому что они не могут быть выполнены компилятором и должны выполняться вручную? С другой стороны, есть ли какие-либо методы, которых мне следует избегать, поскольку они могут помешать способности компилятора автоматически оптимизировать код?

Когда вы оптимизируете на этом низком уровне, существует несколько надежных практических правил. Компиляторы будут отличаться. Измерьте свое текущее решение и решите, не слишком ли оно медленное. Если это так, придумайте гипотезу (например, «Что, если я заменю внутренние операторы if таблицей поиска?»). Это может помочь («устраняет задержки из-за неудачных предсказаний ветвлений») или может повредить («шаблон доступа с поиском ухудшает согласованность кеша»). Экспериментируйте и измеряйте постепенно.

Я часто клонирую простую реализацию и использую #ifdef HAND_OPTIMIZED / #else / #endif для переключения между эталонной версией и измененной версия. Это полезно для последующего обслуживания и проверки кода. Я передаю каждый успешный эксперимент, чтобы изменить управление, и веду журнал (электронную таблицу) с номером списка изменений, временем выполнения и объяснениями для каждого шага оптимизации. По мере того, как я узнаю больше о том, как ведет себя код, журнал упрощает резервное копирование и разветвление в другом направлении.

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

2
ответ дан 26 November 2019 в 21:17
поделиться

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

Общий процесс:

  • Научитесь полюбить Disassembly View в вашем отладчике, или пусть ваша система сборки сгенерирует промежуточные файлы сборки (.s), если это вообще возможно. Следите за изменениями или вещами, которые выглядят вопиюще - даже без знакомства с данной архитектурой набора инструкций вы сможете довольно ясно видеть некоторые вещи! (Иногда я проверяю серию файлов .s с соответствующими изменениями .cpp / .c, просто чтобы использовать прекрасные инструменты из моего SCM, чтобы наблюдать, как код и соответствующий asm меняются с течением времени.)
  • Получите профилировщик, который может следите за счетчиками производительности вашего процессора или хотя бы можете угадывать промахи в кэше. (AMD CodeAnalyst, cachegrind, vTune и т. Д.)

Некоторые другие особенности:

  • Понимание строгого псевдонима. Как только вы это сделаете, используйте restrict , если он есть в вашем компиляторе. (И здесь тоже рассмотрите ошибку!)
  • Ознакомьтесь с различными режимами с плавающей запятой на вашем процессоре и компиляторе. Если денормализованный диапазон вам не нужен, выбор режима без него может улучшить производительность. (Похоже, что вы уже сделали кое-что в этой области, исходя из вашего обсуждения режимов округления.)
  • Определенно избегайте аллоки: вызов резерв на std :: vector , если вы можете, или используйте std :: array , если вы знаете размер во время компиляции.
  • Используйте пулы памяти для увеличения локальности и уменьшения накладных расходов на выделение / свободное пространство; также для обеспечения выравнивания строки кэша и предотвращения пинг-понга.
  • Используйте распределители кадров , если вы распределяете объекты по предсказуемым шаблонам и можете позволить себе освободить все сразу.
  • Следует ли знать об инвариантах . То, что вы знаете, инвариантно, может не относиться к компилятору, например, использование структуры или члена класса в цикле. Я считаю, что самый простой способ выработать правильную привычку - давать всему имя и предпочитаю называть вещи вне циклов. Например. const int threshold = m_currentThreshold; или, возможно, Thing * const pThing = pStructHoldingThing-> pThing; К счастью, вы обычно можете увидеть вещи, которые нуждаются в этой обработке, в представлении разборки. Это также помогает при отладке позже (заставляет окно watch / locals вести себя намного лучше в отладочных сборках)!
  • По возможности избегайте циклической записи - сначала накапливайте, затем записывайте, или группируйте несколько записей вместе. YMMV, конечно.

Напишите свой std :: priority_queue вопрос: вставка объектов в вектор (бэкэнд по умолчанию для priority_queue) имеет тенденцию перемещать множество элементов. Если вы можете разбить на этапы, когда вы вставляете данные, затем сортируете их, а затем читаете после сортировки, вам, вероятно, будет намного лучше. Хотя вы определенно потеряете локальность, вы можете найти более самоупорядочивающуюся структуру, такую ​​как std :: map или std :: set, которая стоит накладных расходов - но это на самом деле зависит от ваших шаблонов использования.

20
ответ дан 26 November 2019 в 21:17
поделиться

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

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

Вот пример такого процесса.

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

Другие говорили, что прежде чем делать что-то еще, сделайте профиль. Я согласен с этим подходом, но я думаю, что есть лучший способ, и он указан в этой ссылке. Всякий раз, когда я спрашиваю себя, может ли какая-то конкретная вещь, например STL, быть проблемой, я просто могу быть прав - НО - я предполагаю. Основная идея победы в настройке производительности - выяснить, а не гадать. Легко выяснить, на что уходит время, поэтому не надо гадать.

2
ответ дан 26 November 2019 в 21:17
поделиться

Если вы работаете, например, с большими матрицами, подумайте о том, чтобы выложить циклы плиткой для улучшения локальности. Это часто приводит к значительным улучшениям. Вы можете использовать VTune/PTU для мониторинга промахов кэша L2.

0
ответ дан 26 November 2019 в 21:17
поделиться

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

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

1
ответ дан 26 November 2019 в 21:17
поделиться

Есть ли какие-либо преимущества в замене контейнеров STL / алгоритмы с ручными?
Я бы рассмотрел это только как последний вариант. Контейнеры и алгоритмы STL были тщательно протестированы. Создание новых требует больших затрат времени на разработку.

Аналогичным образом, для std :: vectors, необходимые размеры которых неизвестны, но имеют достаточно небольшую верхнюю границу, выгодно ли заменять их статически распределенными массивами?
Во-первых, попробуйте зарезервировать место для векторов. Ознакомьтесь с методом std :: vector :: reserve . Вектор, который постоянно растет или изменяется в сторону больших размеров, будет тратить впустую динамическую память и время выполнения. Добавьте код, чтобы определить подходящее значение для верхней границы.

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

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

Вы можете увидеть более значительный прирост производительности, настроив свою программу для оптимизации кэша данных . Найдите в Интернете Data Driven Design . На эту тему есть много отличных статей.

Учитывая научный характер программы, числа с плавающей запятой используются повсеместно. Существенным узким местом в моем коде было преобразование из числа с плавающей запятой в целые числа: компилятор выдавал код для сохранения текущего режима округления, изменял его, выполнял преобразование, а затем восстанавливал старый режим округления --- даже если в программе ничего не было. когда-либо менял режим округления! Отключение этого поведения значительно ускорило мой код. Есть ли какие-нибудь похожие подводные камни, связанные с плавающей запятой, о которых мне следует знать?
Для точности сохраните все как double .Регулируйте округление только при необходимости и, возможно, перед отображением. Это подпадает под правило оптимизации Используйте меньше кода, исключите посторонний или мертвый код .

Также см. Раздел выше о резервировании места в контейнерах перед их использованием.

Некоторые процессоры могут загружать и сохранять числа с плавающей запятой либо быстрее, либо так же быстро, как целые числа. Это потребует сбора данных профиля перед оптимизацией. Однако, если вы знаете, что существует минимальное разрешение, вы можете использовать целые числа и изменить свою базу на это минимальное разрешение. Например, при работе с деньгами США целые числа могут использоваться для обозначения 1/100 или 1/1000 доллара.

Одним из последствий раздельной компиляции и компоновки C ++ является то, что компилятор не может выполнять то, что казалось бы очень простой оптимизацией, например, перемещать вызовы методов, таких как strlen (), из условий завершения цикла. Есть ли какие-то оптимизации, подобные этой, на которые я должен обратить внимание, потому что они не могут быть выполнены компилятором и должны выполняться вручную?
Это неверное предположение. Компиляторы могут оптимизировать на основе сигнатуры функции, особенно если в параметрах правильно используется const . Мне всегда нравится помогать компилятору перемещать константы за пределы цикла. Для значения верхнего предела, такого как длина строки, присвойте его переменной const перед циклом. Модификатор const поможет оптимизатору.

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

С другой стороны,есть ли какие-то приемы, которых мне следует избегать, поскольку они могут помешать компилятору автоматически оптимизировать код?
Я бы избегал «микрооптимизаций». Если у вас есть какие-либо сомнения, распечатайте ассемблерный код, сгенерированный компилятором (для интересующей вас области) при максимальных настройках оптимизации. Попробуйте переписать код, чтобы выразить код сборки компилятора. По возможности оптимизируйте этот код. Для чего-то еще требуются инструкции для конкретной платформы.

Идеи и концепции оптимизации

1. Компьютеры предпочитают выполнять последовательные инструкции.
Ветвление их расстраивает. Некоторые современные процессоры имеют достаточно кеша инструкций, чтобы содержать код для небольших циклов. Если сомневаетесь, не создавайте веток.

2. Устранение требований
Меньше кода, больше производительность.

3. Оптимизируйте дизайн до кода Часто более высокую производительность можно получить, изменив дизайн, а не изменив его реализацию. Меньше дизайна способствует меньшему количеству кода, повышает производительность.

4. Подумайте об организации данных Оптимизируйте данные.
Организуйте часто используемые поля в подструктуры . Установите размеры данных так, чтобы они помещались в строку кэша данных . Удалите постоянные данные из структур данных.
По возможности используйте спецификатор const .

5. Рассмотрите возможность замены страниц Операционные системы заменят вашу программу или задачу другой. Часто в «файл подкачки» на жестком диске. Разделение кода на части, которые содержат сильно исполняемый код и менее исполняемый код, поможет ОС.Кроме того, коагулируйте часто используемый код в более жесткие блоки. Идея состоит в том, чтобы уменьшить количество подкачки кода с жесткого диска (например, получение "удаленных" функций). Если код необходимо заменить, он должен быть как единое целое.

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

7. Устранение конкуренции
Избавьтесь от любых программ и задач, которые конкурируют с вашим приложением за процессор (ы). Такие задачи, как проверка на вирусы и воспроизведение музыки. Даже драйверы ввода-вывода хотят получить часть действия (вот почему вы хотите уменьшить количество транзакций ввода-вывода).

Это должно занять у вас какое-то время. : -)

13
ответ дан 26 November 2019 в 21:17
поделиться

Вот кое-что, что помогло мне однажды. Я не могу сказать, что это сработает для вас. У меня был код в строках

switch(num) {
   case 1: result = f1(param); break;
   case 2: result = f2(param); break;
   //...
}

Затем я получил серьезный прирост производительности, когда изменил его на

// init:
funcs[N] = {f1, f2 /*...*/};
// later in the code:
result = (funcs[num])(param);

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

-1
ответ дан 26 November 2019 в 21:17
поделиться

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

Встроенные функции Google SSE для получения дополнительной информации об этом.

-1
ответ дан 26 November 2019 в 21:17
поделиться

Одним из последствий того, что C++ компилируется и компонуется отдельно, является то, что компилятор не может сделать, казалось бы, очень простые оптимизации, такие как перемещение вызовов методов типа strlen() из условий завершения цикла. Существуют ли какие-либо оптимизации, подобные этой, на которые мне следует обратить внимание, поскольку они не могут быть сделаны компилятором и должны быть сделаны вручную?

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

  • компиляторы Microsoft Visual C++
  • Intel C++ Compiler
  • LLVC-GCC
  • GCC (кажется, не уверен)
0
ответ дан 26 November 2019 в 21:17
поделиться

Мой текущий проект - это медиа-сервер с многопоточной обработкой (язык C ++). Это критичное ко времени приложение, поскольку функции с низкой производительностью могут привести к плохим результатам при потоковой передаче мультимедиа, например, к потере синхронизации, высокой задержке, огромным задержкам и т. Д.

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

Сначала я написал свои собственные классы STL, управления сетью и файлами.

Все мои классы контейнеров («MySTL») управляют своими собственными блоками памяти, чтобы избежать множественных вызовов alloc (new) / free (delete). Освобожденные объекты помещаются в очередь в пуле блоков памяти для повторного использования при необходимости.Таким образом я улучшаю производительность и защищаю свой код от фрагментации памяти.

Части кода, которым необходим доступ к системным ресурсам с низкой производительностью (например, файлы, базы данных, скрипт, запись по сети), я использую для них отдельные потоки. Но не один поток для каждого модуля (например, не 1 поток для каждого сокета), в этом случае операционная система потеряет производительность при управлении большим количеством потоков. Таким образом, вы можете сгруппировать объекты одного класса для обработки в отдельном потоке, если это возможно.

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

В конце концов, инструмент профилирования будет полезен для поиска бутылок с программами и показывает, какие алгоритмы следует улучшить.

Я добился успеха, используя эту стратегию, когда у меня были серверы, работающие более 500 дней на Linux-машине без перезагрузки, с тысячами пользователей, заходящими в систему каждый день.

[02:01] -alpha.ip.tv- Время работы: 525 дней 12 часов 43 минуты 7 секунд

-1
ответ дан 26 November 2019 в 21:17
поделиться

Есть ли польза от замены STL? контейнеры / алгоритмы с ручным управлением те? В частности, моя программа включает очень большую очередь приоритетов (в настоящее время std :: priority_queue) чьи манипуляции требуют много общее время. Это что-то стоит изучает, или STL реализация уже вероятно как можно быстрее?

STL, как правило, является самым быстрым в общем случае. Если у вас очень конкретный случай, вы можете увидеть ускорение с помощью ручного ската. Например, std :: sort (обычно быстрая сортировка) - это самая быстрая общая сортировка, но если вы заранее знаете, что ваши элементы практически уже упорядочены, то сортировка вставкой может быть лучшим выбором.

Аналогично для std :: vectors чьи необходимые размеры неизвестны, но имеют достаточно небольшую верхнюю границу, выгодно ли заменить их на статически распределенные массивы?

Это зависит от того, где вы собираетесь делать статическое размещение. Одна вещь, которую я пробовал в этом направлении, - статическое выделение большого объема памяти в стеке, а затем повторное использование позже. Полученные результаты? Память кучи была значительно быстрее. Тот факт, что элемент находится в стеке, не ускоряет доступ к нему - скорость памяти стека также зависит от таких вещей, как кеш. Статически распределенный глобальный массив не может быть быстрее, чем куча. Я предполагаю, что вы уже пробовали такие приемы, как просто резервирование верхней границы. Если у вас много векторов с одинаковой верхней границей, подумайте об улучшении кеширования, создав вектор структур, содержащих элементы данных.

Я обнаружил, что динамическая память выделение часто бывает тяжелым узкое место, и это устраняет его может привести к значительному ускорению. Как следствие меня интересует в компромиссы производительности при возврате большие временные структуры данных значение vs. возврат по указателю vs. передача результата по ссылке. Является есть способ надежно определить будет ли компилятор использовать RVO для данного метода (при условии вызывающему абоненту не нужно изменять результат, конечно)?

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

Как компиляторы, работающие с кешем, стремятся быть? Например, стоит ли искать переупорядочить вложенные циклы?

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

Учитывая научный характер программа, числа с плавающей запятой используется везде.Значительный узким местом в моем коде раньше было преобразования из числа с плавающей запятой в целые числа: компилятор выдаст код чтобы сохранить текущий режим округления, изменить его, выполнить преобразование, затем восстановите старый режим округления --- хотя ничего в программе когда-либо менял режим округления! Значительное отключение этого поведения ускорил мой код. Есть ли похожие Ошибки, связанные с плавающей запятой I следует знать о?

Существуют разные модели с плавающей запятой - Visual Studio предоставляет параметр компилятора fp: fast. Что касается точного эффекта от этого, я не могу быть уверен. Однако вы можете попробовать изменить точность с плавающей запятой или другие настройки в вашем компиляторе и проверить результат.

Одно из последствий компиляции C ++ и отдельно связано то, что компилятор не может сделать то, что кажутся очень простыми оптимизациями, например, вызовы метода перемещения, такие как strlen () из завершения условия цикла. Есть ли оптимизация, подобная той, которую я следует остерегаться, потому что они не могут должно выполняться компилятором и должно быть сделано вручную?

Я никогда не встречал такого сценария. Однако, если вас это действительно беспокоит, можно сделать это вручную. Одна из вещей, которую вы можете попробовать, - это вызвать функцию по ссылке на константу, внушая компилятору, что значение не изменится.

Еще одна вещь, на которую я хочу обратить внимание, - это использование нестандартных расширений компилятора, например, предоставляемых Visual Studio, __assume. http://msdn.microsoft.com/en-us/library/1b3fsfxw (VS.80).aspx

Есть также многопоточность, и я ожидаю, что вы пошли по этому пути. Вы можете попробовать некоторые конкретные варианты, например, другой ответ, предлагающий SSE.

Редактировать: Я понял, что многие из опубликованных мной предложений напрямую относятся к Visual Studio. Это правда, но GCC почти наверняка предоставляет альтернативы большинству из них. У меня просто личный опыт работы с VS больше всего.

0
ответ дан 26 November 2019 в 21:17
поделиться

И я думаю, что главная подсказка, которую может дать вам любой человек, это: measure, measure, measure. Это и улучшение ваших алгоритмов.
То, как вы используете определенные возможности языка, версия компилятора, реализация std lib, платформа, машина - все это играет свою роль в производительности, и вы не упомянули многие из них, и ни у кого из нас никогда не было вашей точной установки.

Что касается замены std::vector: используйте drop-in замену (например, этот) и просто попробуйте.

1
ответ дан 26 November 2019 в 21:17
поделиться

О контейнерах STL.

Большинство людей утверждают, что STL предлагает одну из самых быстрых реализаций контейнерных алгоритмов. И я говорю обратное: для большинства реальных сценариев контейнеры STL, взятые как есть, дают действительно катастрофическую производительность .

Люди спорят о сложности алгоритмов, используемых в STL. Здесь STL хорош: O (1) для list / queue , vector (с амортизацией) и O (log (N)) для map . Но это не настоящее узкое место для производительности типичного приложения! Для многих приложений реальным узким местом являются операции кучи ( malloc / free , new / delete и т. Д. .).

Типичная операция со списком требует всего несколько циклов ЦП. На карте - несколько десятков, может быть больше (конечно, это зависит от состояния кеша и журнала (N)). А типичные операции с кучей стоят от сотен до тысяч (!!!) циклов процессора. Например, для многопоточных приложений также требуется синхронизация (взаимосвязанные операции). Кроме того, в некоторых ОС (например, Windows XP) функции кучи полностью реализованы в режиме ядра.

Таким образом, фактическая производительность контейнеров STL в типичном сценарии определяется количеством выполняемых ими операций с кучей. И вот они катастрофические. Не потому, что они плохо реализованы, а из-за их дизайна . То есть это вопрос дизайна.

С другой стороны, есть и другие контейнеры, которые сконструированы иначе. Когда-то я спроектировал и написал такие контейнеры для своих нужд:

http://www.codeproject.com/KB/recipes/Containers.aspx

И это оказалось для меня лучше с точки зрения производительности вид и не только.

Но недавно я обнаружил, что я не единственный, кто думал об этом. boost :: intrusive - это контейнерная библиотека, которая реализована аналогично тому, что я сделал тогда.

Предлагаю вам попробовать (если вы еще этого не сделали)

3
ответ дан 26 November 2019 в 21:17
поделиться

вот некоторые вещи, которые я использовал:

  • шаблоны для определения границ самых внутренних циклов (что делает их действительно быстрыми)
  • использование __ restrict __ ключевых слов для проблем с псевдонимами
  • заранее зарезервируют векторы для разумных значений по умолчанию .
  • избегайте использования карты (это может быть очень медленно)
  • добавление / вставка вектора может быть значительно медленным. В этом случае необработанные операции могут ускорить
  • выравнивание N-байтовой памяти (Intel согласовала прагму, http://www.intel.com/software/products/compilers/docs/clin/main_cls /cref_cls/common/cppref_pragma_vector.htm)
  • попытка сохранить память в кэшах L1 / L2.
  • скомпилирован с профилем NDEBUG
  • с использованием oprofile, используйте opannotate для поиска определенных строк (тогда отчетливо видны служебные данные stl)

вот примеры частей данных профиля (чтобы вы знали, где искать проблемы)

 * Output annotated source file with samples
 * Output all files
 *
 * CPU: Core 2, speed 1995 MHz (estimated)
--
 * Total samples for file : "/home/andrey/gamess/source/blas.f"
 *
 * 1020586 14.0896
--
 * Total samples for file : "/home/andrey/libqc/rysq/src/fock.cpp"
 *
 * 962558 13.2885
--
 * Total samples for file : "/usr/include/boost/numeric/ublas/detail/matrix_assign.hpp"
 *
 * 748150 10.3285

--
 * Total samples for file : "/usr/include/boost/numeric/ublas/functional.hpp"
 *
 * 639714  8.8315
--
 * Total samples for file : "/home/andrey/gamess/source/eigen.f"
 *
 * 429129  5.9243
--
 * Total samples for file : "/usr/include/c++/4.3/bits/stl_algobase.h"
 *
 * 411725  5.6840
--

пример кода из моего проекта

template<int ni, int nj, int nk, int nl>
inline void eval(const Data::density_type &D, const Data::fock_type &F,
                 const double *__restrict Q, double scale) {

    const double * __restrict Dij = D[0];
    ...
    double * __restrict Fij = F[0];
    ...

    for (int l = 0, kl = 0, ijkl = 0; l < nl; ++l) {
        for (int k = 0; k < nk; ++k, ++kl) {
            for (int j = 0, ij = 0; j < nj; ++j, ++jk, ++jl) {
                for (int i = 0; i < ni; ++i, ++ij, ++ik, ++il, ++ijkl) {
2
ответ дан 26 November 2019 в 21:17
поделиться
Другие вопросы по тегам:

Похожие вопросы: