Важная справочная информация: микроарх pdf Агнера Фога и, вероятно, также Ульрих Дреппер, что каждый программист должен знать о памяти . Смотрите также другие ссылки вx86tag wiki, особенно руководства по оптимизации Intel, и анализ Дэвидом Кантером микроархитектуры Haswell с диаграммами .
Очень классное задание; намного лучше, чем те, которые я видел, где студентов просили оптимизировать некоторый кодgcc -O0
, изучая кучу трюков, которые не имеют значения в реальном коде. В этом случае вас просят узнать о конвейере ЦП и использовать его для руководства вашими усилиями по де-оптимизации, а не только для слепых предположений. Самая забавная часть этого оправдывает каждую пессимизацию "дьявольской некомпетентностью", а не преднамеренной злобой.
Проблемы с назначением формулировки и кода :
Параметры, специфичные для uarch, для этого кода ограничены. Он не использует никаких массивов, и большая часть затрат - это вызов функций exp
/ log
библиотеки. Не существует очевидного способа иметь более или менее параллелизм на уровне команд, и цепочка зависимостей, переносимых циклами, очень коротка.
Я хотел бы увидеть ответ, в котором предпринята попытка замедлить процесс реорганизации выражений, чтобы изменить зависимости, уменьшить ILP только из зависимостей (опасностей). Я не пытался это сделать.
Процессоры семейства Intel Sandybridge представляют собой агрессивные нестандартные конструкции, которые расходуют много транзисторов и мощности для нахождения параллелизма и избегания опасностей (зависимостей), которые могли бы создать проблему для классического конвейера RISC . Обычно единственными традиционными опасностями, которые замедляют его, являются «истинные» зависимости RAW, которые приводят к тому, что пропускная способность ограничивается задержкой.
Опасности для регистров WAR и WAW в значительной степени не являются проблемой благодаря переименованию регистров . (за исключениемpopcnt
/lzcnt
/tzcnt
, которые имеют ложную зависимость своего назначения от процессоров Intel , даже если это только для записи. То есть WAW обрабатывается как опасность RAW + запись). Для упорядочения памяти современные процессоры используют очереди хранилищ, чтобы задержать фиксацию в кеше до выхода на пенсию, также избегая опасностей WAR и WAW .
Почему Мулсс занимает всего 3 цикла в Haswell, в отличие от таблиц инструкций Агнера? больше о переименовании регистров и сокрытии задержки FMA в цикле FP.
Фирменное наименование «i7» было представлено с Nehalem (преемником Core2) , и некоторые руководства Intel даже говорят «Core i7», когда они, кажется, означают Nehalem, но они сохранили марку «i7» для Sandybridge и более поздних микроархитектур. SnB - это когда P6-семейство превратилось в новый вид, SnB-семейство . Во многих отношениях Nehalem имеет больше общего с Pentium III, чем с Sandybridge (например, сбои чтения регистров и остановки чтения ROB не происходят на SnB, потому что он изменился на использование физического файла регистров. Также кэш UOP и другой внутренний формат UOP). Термин «архитектура i7» бесполезенпотому что нет смысла группировать SnB-семью с Nehalem, но не с Core2. (Nehalem действительно представил общую кэш-архитектуру L3 с инклюзивным доступом для соединения нескольких ядер друг с другом. А также с интегрированными графическими процессорами. Таким образом, на уровне чипов наименование имеет больше смысла.)
Резюме хороших идей, которые может оправдать дьявольская некомпетентность
Даже дьявольски некомпетентные люди вряд ли добавят заведомо бесполезную работу или бесконечный цикл, а создание беспорядка с классами C ++ / Boost выходит за рамки назначения.
- Многопоточность с одним общим
std::atomic<uint64_t>
счетчиком циклов, поэтому происходит правильное общее количество итераций. Атомная uint64_t особенно плохо с -m32 -march=i586
. Чтобы получить бонусные баллы, сделайте так, чтобы они были выровнены, и пересечение границы страницы неравномерным (не 4: 4).
- Ложный общий доступ для некоторых других неатомарных переменных -> конвейер ошибочных спекуляций порядка памяти очищает, а также лишние ошибки кэширования.
- Вместо того, чтобы использовать
-
переменные FP, XOR старшего байта с 0x80, чтобы перевернуть бит знака, вызывая остановку пересылки магазина .
- Время каждой итерации независимо, с чем-то еще тяжелее, чем
RDTSC
. например CPUID
/ RDTSC
или функция времени, которая делает системный вызов. Инструкции по сериализации по своей сути являются недружественными.
- Измените умножения на константы на деления на их взаимные («для удобства чтения»). div медленный и не полностью конвейеризованный.
- Векторизовать умножение / sqrt с AVX (SIMD), но не использовать
vzeroupper
перед вызовами скалярной математической библиотеки exp()
и log()
функций, что приводит к остановке перехода AVX <-> SSE .
- Сохраните выходные данные ГСЧ в связанном списке или в массивах, которые вы просматриваете не по порядку. То же самое для результата каждой итерации, и сумма в конце.
Также рассматривается в этом ответе, но исключается из резюме: предложения, которые были бы такими же медленными для непотрубного процессора, или которые не кажутся оправданными даже при дьявольской некомпетентности. например, много идей gimp-the-compiler, которые приводят к явно другому / худшему асму.
Многопоточность плохо
Возможно, используйте OpenMP для многопоточных циклов с очень небольшим количеством итераций, с гораздо большими издержками, чем прирост скорости. Ваш код Монте-Карло имеет достаточно параллелизма, чтобы на самом деле получить ускорение, тем не менее, особенно. если нам удастся сделать каждую итерацию медленной. (Каждый поток вычисляет частичное payoff_sum
, добавленное в конце). #omp parallel
в этом цикле, вероятно, будет оптимизация, а не пессимизация.
Многопоточность, но вынуждает оба потока использовать один и тот же счетчик цикла (с atomic
приращениями, чтобы общее число итераций было правильным) Это кажется дьявольски логичным. Это означает использование static
переменной в качестве счетчика цикла. Это оправдывает использование atomic
счетчиков циклов и создает фактический пинг-понг на линии кэша (если потоки не работают на одном физическом ядре с гиперпоточностью; это может быть не так медленно). Во всяком случае, это гораздо медленнее, чем необоснованный случай lock inc
. А lock cmpxchg8b
для атомарного увеличения числа участников uint64_t
в 32-битной системе придется повторять цикл, вместо того, чтобы аппаратный арбитр обрабатывал атом inc
.
Также создайте ложное совместное использование , где несколько потоков хранят свои личные данные (например, состояние RNG) в разных байтах одной и той же строки кэша. (Учебное пособие Intel об этом, включая счетчики перфорации, чтобы посмотреть) . В этом есть специфический аспект микроархитектуры : процессоры Intel спекулируют на том, что не происходит неправильного упорядочения памяти , и есть событие машинного сброса порядка порядка памяти, чтобы обнаружить это, по крайней мере, на P4 . Наказание может быть не таким большим на Haswell. Как указывает эта ссылка, lock
инструкция ed предполагает, что это произойдет, избегая неправильных предположений. Нормальная загрузка предполагает, что другие ядра не будут делать недействительной строку кэша между тем, когда загрузка выполняется, и когда она удаляется в программном порядке (если вы не используетеpause
). Правильный обмен без lock
инструкций ed - это обычно ошибка. Было бы интересно сравнить неатомарный счетчик общего цикла с атомарным случаем. Чтобы по-настоящему пессимизировать, сохраняйте счетчик общего атомарного цикла и вызывайте ложное совместное использование в той же или другой строке кэша для некоторой другой переменной.
Случайные уарх-специфические идеи:
Если вы можете ввести какие-либо непредсказуемые ветки , это существенно снизит код. Современные процессоры x86 имеют довольно длинные конвейеры, поэтому ошибочный прогноз стоит ~ 15 циклов (при запуске из кэша UOP).
Цепочки зависимостей:
Я думаю, что это была одна из предполагаемых частей задания.
Поражение способности ЦП использовать параллелизм на уровне команд путем выбора порядка операций, который имеет одну длинную цепочку зависимостей вместо нескольких коротких цепочек зависимостей. Компиляторам не разрешается изменять порядок операций для вычислений FP, если вы не используете их -ffast-math
, потому что это может изменить результаты (как описано ниже).
Чтобы действительно сделать это эффективным, увеличьте длину цепочки зависимостей, переносимых циклами. Тем не менее, ничто не выглядит так очевидно: циклы, как написано, имеют очень короткие цепочки зависимостей, переносимых циклами: просто добавление FP. (3 цикла). Множественные итерации могут иметь свои вычисления в полете одновременно, потому что они могут начаться задолго до payoff_sum +=
конца предыдущей итерации. ( log()
и exp
принять много инструкций, но не намного больше, чем окно не в порядке Haswell для нахождения параллелизма: размер ROB = 192 мопов в слитой области и размер планировщика = 60 мопов в неиспользуемой области, Как только выполнение текущей итерации продвигается достаточно далеко, чтобы освободить место для инструкций следующей следующей итерации, любые ее части, у которых есть готовые входные данные (т. Е. Независимая / отдельная цепь депозита), могут начать выполняться, когда более старые инструкции покидают блоки выполнения. бесплатно (например, потому что они имеют узкое место по задержке, а не по пропускной способности).
Состояние RNG почти наверняка будет более длинной цепочкой зависимостей, чем перенос addps
.
Используйте более медленные / больше операций FP (особенно больше деления):
Разделите на 2,0 вместо умножения на 0,5 и так далее. Умножение FP сильно конвейеризовано в разработках Intel и имеет пропускную способность на 0.5c в Haswell и более поздних версиях. FP divsd
/ divpd
только частично конвейеризован . (Хотя Skylake имеет впечатляющую пропускную способность на 4c для divpd xmm
задержки с 13-14c, в отличие от Nehalem (7-22c)).
do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);
Ясно тестирование на расстоянии, так ясно , что было бы правильно , чтобы sqrt()
это. : P ( sqrt
еще медленнее, чем div
).
Как предполагает @Paul Clayton, переписывание выражений с ассоциативными / дистрибутивными эквивалентами может принести больше работы (если вы не используете -ffast-math
его для повторной оптимизации компилятора). (exp(T*(r-0.5*v*v))
может стать exp(T*r - T*v*v/2.0)
. Обратите внимание, что хотя математика для вещественных чисел является ассоциативной, математика с плавающей запятой - нет , даже без учета переполнения / NaN (поэтому -ffast-math
по умолчанию она не включена). Смотрите комментарий Павла для очень волосатого вложенного pow()
предложения.
Если вы можете уменьшить вычисления до очень маленьких чисел, то математические операции FP потребуют ~ 120 дополнительных циклов, чтобы перейти в микрокод, когда операция с двумя нормальными числами приводит к денормализации . Посмотрите микроархитектору Агнера Фога pdf для точных чисел и деталей. Это маловероятно, поскольку у вас много множителей, поэтому коэффициент масштабирования будет возведен в квадрат и уменьшен до 0,0. Я не вижу способа оправдать необходимое масштабирование некомпетентностью (даже дьявольской), только преднамеренной злобой.
Если вы можете использовать intrinsics ( <immintrin.h>
)
Используйте movnti
для удаления ваших данных из кэша . Diabolical: он новый и слабо упорядоченный, так что процессор должен работать быстрее, верно? Или посмотрите этот связанный вопрос для случая, когда кто-то был в опасности сделать именно это (для разрозненных записей, где только в некоторых местах было жарко). clflush
вероятно, невозможно без злого умысла.
Используйте целочисленные тасования между математическими операциями FP, чтобы вызвать задержки обхода.
Смешивание инструкций SSE и AVX без надлежащего использования vzeroupper
приводит к большим остановкам в пред-Skylake (и другой штраф в Skylake ). Даже без этого плохая векторизация может быть хуже скалярной (больше циклов тратится на перетасовку данных в / из векторов, чем на сохранение, выполняя операции add / sub / mul / div / sqrt для 4 итераций Монте-Карло одновременно с 256b векторами) , Модули выполнения add / sub / mul полностью конвейерны и имеют полную ширину, но div и sqrt для векторов 256b не так быстры, как для векторов 128b (или скаляров), поэтому ускорение не является существеннымdouble
.
exp()
и log()
не имеют аппаратной поддержки, так что для этой части потребуется извлечь векторные элементы обратно в скаляр и вызвать функцию библиотеки отдельно, а затем перетасовать результаты обратно в вектор. libm обычно компилируется только для использования SSE2, поэтому будет использовать устаревшие SSE-кодировки скалярных математических инструкций. Если в вашем коде используются векторы 256b и вызовы exp
без выполнения vzeroupper
первого, то вы останавливаетесь. После возврата инструкция AVX-128, например, vmovsd
для установки следующего векторного элемента в качестве аргумента for exp
, также будет остановлена. И затем exp()
снова остановится при выполнении инструкции SSE. Это именно то, что произошло в этом вопросе , вызвав 10-кратное замедление. (Спасибо @ZBoson).
См. Также эксперимент Натана Курца с математической библиотекой Intel и glibc для этого кода . Будущий glibc будет поставляться с векторизованными реализациями exp()
и так далее.
Если нацелен на pre-IvB или esp. Нехалем, попробуй заставить gcc вызвать частичные задержки в регистре с 16-битными или 8-битными операциями, за которыми следуют 32-битные или 64-битные операции. В большинстве случаев gcc будет использовать movzx
после 8- или 16-битной операции, но в данном случае gcc изменяет ah
и затем читаетax
С (встроенным) asm:
С помощью (встроенного) asm вы можете разбить кеш uop: 32-килобайтный фрагмент кода, который не помещается в три строки кеша 6uop, вызывает переключение с кеша uop на декодеры. Некомпетентное ALIGN
использование множества однобайтовых nop
s вместо пары long nop
s на цели ветвления во внутреннем цикле может помочь. Или поместите выравнивающий отступ после метки, а не до. : P Это имеет значение только в том случае, если внешний интерфейс является узким местом, чего не будет, если мы преуспеем в пессимизации остальной части кода.
Используйте самоизменяющийся код для запуска очистки конвейера (также называемой машинным ядром).
LCP-киоски из 16-битных инструкций с непосредственными значениями, слишком большими, чтобы поместиться в 8-битные, вряд ли будут полезны. Кэш UOP в SnB и более поздних версиях означает, что вы платите штраф за декодирование только один раз. На Nehalem (первый i7) он может работать для цикла, который не помещается в буфер цикла на 28 моп. Иногда gcc генерирует такие инструкции, даже -mtune=intel
если и когда он мог использовать 32-битную инструкцию.
Распространенной идиомой для определения времени является CPUID
(для сериализации) тогдаRDTSC
. Время каждой итерации отдельно с CPUID
/, RDTSC
чтобы убедиться, что RDTSC
не переупорядочено с более ранними инструкциями, что сильно замедлит ход . (В реальной жизни разумный способ рассчитать время - это провести все итерации по времени, вместо того, чтобы рассчитывать каждую из них по отдельности и складывать их).
Вызывает много пропусков кэша и других замедлений памяти
Используйте union { double d; char a[8]; }
для некоторых ваших переменных. Вызвать переадресацию магазина, выполнив узкое хранилище (или Read-Modify-Write) только для одного из байтов. (Эта вики-статья также охватывает много других микроархитектурных вещей для очередей загрузки / хранения). Например, переверните знак double
использования XOR 0x80 только для старшего байта вместо -
оператора. Дьявольски некомпетентный разработчик, возможно, слышал, что FP медленнее, чем целое число, и, таким образом, пытается сделать как можно больше, используя целочисленные операции. (Очень хороший компилятор, предназначенный для математики FP в регистрах SSE, может скомпилировать это вxorps
с константой в другом регистре xmm, но для x87 это не страшно, если компилятор понимает, что он отрицает значение, и заменяет следующее сложение вычитанием.)
Используйте, volatile
если вы компилируете, -O3
а не используете std::atomic
, чтобы заставить компилятор фактически хранить / перезагружать повсюду. Глобальные переменные (вместо локальных) также вызовут некоторые сохранения / перезагрузки, но слабый порядок модели памяти C ++ не требует, чтобы компилятор постоянно проливал / перезагружал в память.
Замените локальные переменные членами большой структуры, чтобы вы могли контролировать структуру памяти.
Используйте массивы в структуре для заполнения (и хранения случайных чисел, чтобы оправдать их существование).
Выберите свой макет памяти, чтобы все входило в другую строку в том же «наборе» в кэше L1 . Это только 8-сторонняя ассоциация, то есть каждый набор имеет 8 «путей». Строки кэша 64B.
Более того, поместите вещи точно в 4096B, так как нагрузки имеют ложную зависимость от магазинов на разных страницах, но с одинаковым смещением на странице . Агрессивные неупорядоченные процессоры используют устранение неоднозначности памяти, чтобы выяснить, когда загрузки и хранилища можно переупорядочить без изменения результатов , а реализация Intel имеет ложные срабатывания, которые предотвращают раннее начало загрузки. Вероятно, они проверяют только биты ниже смещения страницы, поэтому проверка может начаться до того, как TLB переведет старшие биты с виртуальной страницы на физическую страницу. Как и руководство Агнера, см. Ответ Стивена Кэнона , а также раздел в конце ответа @Krazy Glew на тот же вопрос. (Энди Глеу был одним из архитекторов оригинальной микроархитектуры P6 от Intel.)
Используйте, __attribute__((packed))
чтобы позволить вам выровнять переменные так, чтобы они перекрывали строки кэша или даже границы страниц. (Таким образом, для загрузки одного double
нужны данные из двух строк кэша). Неверно выровненные загрузки не имеют штрафов в любом Intel i7 uarch, за исключением случаев пересечения строк кэша и строк страницы. Расщепление строк кэша все еще требует дополнительных циклов . Skylake значительно снижает штраф за загрузку страниц с 100 до 5 циклов. (Раздел 2.1.3) . Возможно, связано с возможностью параллельного обхода двух страниц.
Разделение страницы atomic<uint64_t>
должно быть примерно в худшем случае , особенно если это 5 байт на одной странице и 3 байта на другой странице, или что-то кроме 4: 4. Даже расщепления по середине более эффективны для расщепления строк кэша с векторами 16B на некоторых уровнях, IIRC. Поместите все в alignas(4096) struct __attribute((packed))
(для экономии места, конечно), включая массив для хранения результатов ГСЧ. Добиться смещения, используя uint8_t
или uint16_t
для чего-то перед счетчиком.
Если вы можете заставить компилятор использовать индексированные режимы адресации, это победит микроплавление . Может быть, с помощью #define
s заменить простые скалярные переменные на my_data[constant]
.
Если вы можете ввести дополнительный уровень косвенности, чтобы адреса загрузки / хранения не были известны заранее, это может привести к дальнейшей пессимизации.
Массивы перемещений в несмежном порядке
Я думаю, что мы можем придумать некомпетентное обоснование для введения массива в первую очередь: он позволяет нам отделить генерацию случайных чисел от использования случайных чисел. Результаты каждой итерации также могут быть сохранены в массиве для последующего суммирования (с большей дьявольской некомпетентностью).
Для «максимальной случайности» у нас мог бы быть цикл, перебирающий случайный массив и записывающий в него новые случайные числа. Поток, использующий случайные числа, может генерировать случайный индекс для загрузки случайного числа. (Здесь есть некоторая предварительная работа, но микроархитектура помогает заранее определить адреса загрузки, так что любая возможная задержка загрузки может быть решена до того, как потребуются загруженные данные.) Наличие устройства чтения и записи на разных ядрах приведет к неправильному упорядочению памяти конвейер спекуляций очищается (как обсуждалось ранее для случая ложного обмена).
Для максимальной пессимизации зациклите массив с шагом 4096 байт (т.е. 512 удваивается). например
for (int i=0 ; i<512; i++)
for (int j=i ; j<UPPER_BOUND ; j+=512)
monte_carlo_step(rng_array[j]);
Таким образом, шаблон доступа: 0, 4096, 8192, ...,
8, 4104, 8200, ...
16, 4112, 8208, ...
Это то, что вы получили бы за доступ к двумерному массиву, как double rng_array[MAX_ROWS][512]
в неправильном порядке (зацикливание строк, а не столбцов внутри строки во внутреннем цикле, как предложено @JesperJuhl). Если дьявольская некомпетентность может оправдать двумерный массив с такими размерами, то реальная некомпетентность садового разнообразия легко оправдывает зацикливание с неправильным шаблоном доступа. Это происходит в реальном коде в реальной жизни.
При необходимости измените границы цикла, чтобы использовать много разных страниц вместо повторного использования одних и тех же нескольких страниц, если массив не такой большой. Аппаратная предварительная выборка не работает (также / вообще) на всех страницах. Устройство предварительной выборки может отслеживать один прямой и один обратный поток на каждой странице (что здесь происходит), но будет действовать на него только в том случае, если пропускная способность памяти еще не заполнена без предварительной выборки.
Это также приведет к большому количеству пропусков TLB, если только страницы не будут объединены в огромную страницу ( Linux делает это условно для анонимных (не поддерживаемых файлами) размещений, таких как malloc
/ new
которые используютmmap(MAP_ANONYMOUS)
).
Вместо массива для хранения списка результатов вы можете использовать связанный список . Тогда каждая итерация будет требовать загрузки с указателем (реальная опасность зависимости RAW для адреса загрузки следующей загрузки). При плохом распределителе вам, возможно, удастся разбросать узлы списка в памяти, победив кеш. С дьявольски некомпетентным распределителем он может поместить каждый узел в начало своей собственной страницы. (например, выделять mmap(MAP_ANONYMOUS)
напрямую, не разбивая страницы и не отслеживая размеры объектов для правильной поддержки free
).
Они на самом деле не зависят от микроархитектуры и не имеют ничего общего с конвейером (большинство из них также будут замедлять работу нетранслируемого процессора).
Несколько не по теме: заставить компилятор генерировать худший код / делать больше работы:
Используйте C ++ 11 std::atomic<int>
и std::atomic<double>
для самого пессимального кода. MFENCE и lock
инструкции ed довольно медленные, даже без конфликтов из другого потока.
-m32
сделает код медленнее, потому что код x87 будет хуже, чем код SSE2. Основанное на стеке 32-битное соглашение о вызовах принимает больше инструкций и передает даже аргументы FP в стеке таким функциям, как exp()
. atomic<uint64_t>::operator++
на -m32
требует lock cmpxchg8B
петли (i586). (Так что используйте это для счетчиков циклов! [Злой смех]).
-march=i386
также будет пессимизировать (спасибо @Jesper). FP сравнивается fcom
медленнее, чем 686 fcomi
. Pre-586 не предоставляет атомарного 64-битного хранилища (не говоря уже о cmpxchg), поэтому все 64- atomic
битные операции компилируются в вызовы функций libgcc (которые, вероятно, скомпилированы для i686, а не фактически используют блокировку). Попробуйте это по ссылке на Godbolt Compiler Explorer в последнем абзаце.
Используйте long double
/ sqrtl
/ expl
для дополнительной точности и медленности в ABI, где sizeof ( long double
) равен 10 или 16 (с отступом для выравнивания). (IIRC, 64-битная Windows использует 8-байтовый long double
эквивалент double
. (Во всяком случае, загрузка / сохранение 10-байтовых (80-битных) операндов FP составляет 4/7 моп, против float
или double
только принимая 1 моп каждый для fld m64/m32
/ fst
). Форсирование x87 с long double
автоматическим векторизацией побеждает даже для GCC -m64 -march=haswell -O3
.
Если atomic<uint64_t>
счетчики циклов не используются , используйте их long double
для всего, включая счетчики циклов.
atomic<double>
компилируется, но подобные операции чтения-изменения-записи +=
не поддерживаются (даже на 64-битной версии). atomic<long double>
должен вызывать библиотечную функцию только для атомарных загрузок / хранилищ. Вероятно, это действительно неэффективно, потому что x86 ISA не поддерживает атомные 10-байтовые загрузки / хранилища , и единственный способ, которым я могу придумать без lock ( cmpxchg16b
), - это 64-битный режим.
Если -O0
разбить большое выражение, присваивая части временным переменным, это приведет к большему количеству хранилищ / перезагрузок. Без volatile
или что-то, это не будет иметь значения с настройками оптимизации, которые будет использовать реальная сборка реального кода.
Правила char
псевдонимов позволяют a псевдониму чего угодно, поэтому при хранении с помощью char*
компилятора все элементы сохраняются / перезагружаются до / после байтового хранилища, даже в -O3
. (Это проблема для автоматической векторизации кода, который работаетuint8_t
, например, с массивом .)
Попробуйте uint16_t
счетчики циклов для принудительного усечения до 16 бит, возможно, используя 16-битный размер операнда (потенциальные задержки) и / или дополнительные movzx
инструкции (безопасно). Переполнение со знаком является неопределенным поведением , поэтому, если вы не используете -fwrapv
или, по крайней мере -fno-strict-overflow
, счетчики циклов со знаком не должны повторно расширяться при каждой итерации , даже если они используются как смещения для 64-битных указателей.
Принудительное преобразование из целого числа в float
и обратно. И / или double
<=> float
конверсии. Команды имеют задержку больше единицы, и скалярная функция int-> float ( cvtsi2ss
) плохо спроектирована так, чтобы не обнулять остальную часть регистра xmm. (По pxor
этой причине gcc вставляет дополнительные для разрыва зависимостей.)
Часто устанавливайте привязку вашего процессора к другому процессору (предложено @Egwor). дьявольские рассуждения: вы не хотите, чтобы одно ядро перегревалось при долгом запуске потока, не так ли? Возможно, переключение на другое ядро позволит этому ядру работать на более высокой тактовой частоте. (На самом деле: они настолько термически близки друг к другу, что это маловероятно, за исключением системы с несколькими разъемами). Теперь просто сделайте неправильную настройку и делайте это слишком часто. Помимо времени, потраченного на сохранение / восстановление состояния потока ОС, новое ядро имеет холодные кэши L2 / L1, кэши UOP и предикторы ветвления.
Введение частых ненужных системных вызовов может замедлить вас, независимо от того, кто они. Хотя некоторые важные, но простые, например, gettimeofday
могут быть реализованы в пользовательском пространстве без перехода в режим ядра. (glibc в Linux делает это с помощью ядра, поскольку ядро экспортирует код в vdso
).
Для получения дополнительной информации о накладных расходах системных вызовов (включая пропуски кэша / TLB после возврата в пользовательское пространство, а не только самого переключения контекста), в документе FlexSC представлен отличный анализ текущей ситуации, а также предложение по пакетной системе. звонки от массовых многопоточных серверных процессов.
while(true){}