Деоптимизация программы для конвейера в процессорах семейства Intel Sandybridge
Я ломал голову в течение недели, пытаясь выполнить это задание, и я надеюсь, что кто-то здесь может привести меня к правильному пути. Позвольте мне начать с инструкцией инструктора:
ваше задание противоположно нашему первому лабораторному заданию, которое состояло в оптимизации программы простых чисел. Ваша цель в этом задании-пессимизировать программу, т. е. заставить ее работать медленнее. Оба эти ресурсоемкие программы. Они занимают несколько секунд, чтобы запустить на нашем лабораторные ПК. Вы не можете изменить алгоритм.
чтобы деоптимизировать программу, используйте свои знания о том, как работает конвейер Intel i7. Представьте себе способы переупорядочить пути обучения, чтобы ввести войну, сырье и другие опасности. Подумайте о способах минимизации эффективности кэша. Будьте дьявольски некомпетентны.
задание давало выбор программ точильного камня или Монте-Карло. Комментарии эффективности кэша в основном применимы только к точильному камню, но я выбрал программу моделирования Монте-Карло:
// Un-modified baseline for pessimization, as given in the assignment
#include <algorithm> // Needed for the "max" function
#include <cmath>
#include <iostream>
// A simple implementation of the Box-Muller algorithm, used to generate
// gaussian random numbers - necessary for the Monte Carlo method below
// Note that C++11 actually provides std::normal_distribution<> in
// the <random> library, which can be used instead of this function
double gaussian_box_muller() {
double x = 0.0;
double y = 0.0;
double euclid_sq = 0.0;
// Continue generating two uniform random variables
// until the square of their "euclidean distance"
// is less than unity
do {
x = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
y = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
euclid_sq = x*x + y*y;
} while (euclid_sq >= 1.0);
return x*sqrt(-2*log(euclid_sq)/euclid_sq);
}
// Pricing a European vanilla call option with a Monte Carlo method
double monte_carlo_call_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
double S_adjust = S * exp(T*(r-0.5*v*v));
double S_cur = 0.0;
double payoff_sum = 0.0;
for (int i=0; i<num_sims; i++) {
double gauss_bm = gaussian_box_muller();
S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
payoff_sum += std::max(S_cur - K, 0.0);
}
return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}
// Pricing a European vanilla put option with a Monte Carlo method
double monte_carlo_put_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
double S_adjust = S * exp(T*(r-0.5*v*v));
double S_cur = 0.0;
double payoff_sum = 0.0;
for (int i=0; i<num_sims; i++) {
double gauss_bm = gaussian_box_muller();
S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
payoff_sum += std::max(K - S_cur, 0.0);
}
return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}
int main(int argc, char **argv) {
// First we create the parameter list
int num_sims = 10000000; // Number of simulated asset paths
double S = 100.0; // Option price
double K = 100.0; // Strike price
double r = 0.05; // Risk-free rate (5%)
double v = 0.2; // Volatility of the underlying (20%)
double T = 1.0; // One year until expiry
// Then we calculate the call/put values via Monte Carlo
double call = monte_carlo_call_price(num_sims, S, K, r, v, T);
double put = monte_carlo_put_price(num_sims, S, K, r, v, T);
// Finally we output the parameters and prices
std::cout << "Number of Paths: " << num_sims << std::endl;
std::cout << "Underlying: " << S << std::endl;
std::cout << "Strike: " << K << std::endl;
std::cout << "Risk-Free Rate: " << r << std::endl;
std::cout << "Volatility: " << v << std::endl;
std::cout << "Maturity: " << T << std::endl;
std::cout << "Call Price: " << call << std::endl;
std::cout << "Put Price: " << put << std::endl;
return 0;
}
изменения, которые я сделал, казалось, увеличили время выполнения кода на секунду, но я не совсем уверен, что я могу изменить, чтобы остановить конвейер без добавления кода. Точку в правильном направлении было бы здорово, я ценю любые отзывы.
обновление: профессор, который дал это задание, опубликовал некоторые детали
основные выводы:
- это a второй семестр архитектурный класс в местном колледже (с использованием учебника Хеннесси и Паттерсона).
- лабораторные компьютеры имеют процессоры Haswell
- студенты были подвергнуты воздействию
CPUID
инструкции и как определить размер кэша, а также встроенные функции иCLFLUSH
инструкция. - разрешены любые параметры компилятора, а также встроенный asm.
- написание собственного алгоритма квадратного корня было объявлено как находящееся за пределами бледно -
-O0
, и что увеличение времени выполнения на 17% было разумным.
таким образом, похоже, что цель задания состояла в том, чтобы заставить студентов переупорядочить существующую работу, чтобы уменьшить параллелизм на уровне инструкций или что-то в этом роде, но это не плохо, что люди углубились и узнали больше.
имейте в виду, что это вопрос компьютерной архитектуры, а не вопрос о том, как сделать C++ медленным в целом.
1 ответ:
важное значение фон значение: Агнер туман в формате PDF, и, вероятно, также Ульриха Дреппера Что Каждый Программист Должен Знать О Памяти!--99-->. Смотрите также другие ссылки в x86 tag wiki, особенно руководства по оптимизации Intel и Дэвида Кантера анализ микроархитектуры Haswell, с диаграммами.
очень крутое задание; гораздо лучше, чем те, которые я видел, где студентам было предложено оптимизировать код для
gcc -O0
, узнаешь много трюков, которые не имеют значения в реальном коде. В этом случае вас попросят узнать о конвейере ЦП и использовать его для руководства вашими усилиями по деоптимизации, а не просто слепым угадыванием. самая забавная часть этого оправдывает каждую пессимизацию "дьявольской некомпетентностью", а не преднамеренной злобой.
проблемы с формулировкой задания и код:
на уарч-конкретных вариантов для данного кода ограничено. Он не использует никаких массивов, и большая часть стоимости-это вызовы
exp
/log
функции библиотеки. Нет очевидного способа иметь более или менее параллелизм на уровне инструкций, и цепочка зависимостей с циклом очень коротка.я хотел бы увидеть ответ, который попытался получить замедление от перестановки выражений для изменения зависимостей, чтобы уменьшить ILP только из зависимостей (опасности). я еще не пробовал.
процессоры семейства Intel Sandybridge-это агрессивные нестандартные конструкции, которые тратят много транзисторов и мощности, чтобы найти параллелизм и избежать опасностей (зависимостей), которые будут беспокоить классический конвейер RISC in-order. Обычно единственными традиционными опасностями, которые замедляют его, являются необработанные" истинные " зависимости, которые ограничивают пропускную способность задержка.
война и WAW опасности для регистров в значительной степени не проблема, благодаря переименованию регистра. (за исключением
popcnt
/lzcnt
/tzcnt
, которые ложная зависимость их назначения от процессоров Intel, хотя это только для записи. т. е. WAW обрабатывается как необработанная опасность + запись). Для упорядочения памяти современные процессоры используют хранить очереди, чтобы отложить фиксацию в кэш до выхода на пенсию, а также избежать войны и WAW опасности.фирменное наименование "i7 "было введено с Nehalem (преемником Core2), и некоторые руководства Intel даже говорят" Core i7", когда они, кажется, означают Nehalem, но они сохранили брендинг" i7"для Sandybridge и более поздние микроархитектуры. SnB - это когда P6-семейство эволюционировало в новый вид, SnB-семейство. Во многих отношениях Nehalem имеет больше общего с Pentium III, чем с Sandybridge (например, register read stalls и ROB-read stalls не происходит на SnB, потому что он изменился на использование физического файла регистра. Также кэш uop и другой внутренний формат uop). термин "архитектура i7" не является полезным, потому что нет смысла группировать SnB-семейство с Nehalem, но не Core2. (Однако Nehalem представил общую инклюзивную архитектуру кэша L3 для соединения нескольких ядер вместе. А также интегрированные графические процессоры. Так что чип-уровень, именование имеет больше смысла.)
резюме из хороших идей, которые дьявольская некомпетентность может оправдать
даже дьявольски некомпетентные вряд ли добавят явно бесполезную работу или бесконечный цикл, а создание беспорядка с классами C++/Boost выходит за рамки задания.
- Multi-резьбы с одним shared
std::atomic<uint64_t>
счетчик циклов, поэтому правильное общее количество итераций происходит. Atomic uint64_t особенно плох с-m32 -march=i586
. Для получения бонусных очков, организовать его чтобы быть смещенным и пересекать границу страницы с неравномерным разделением (не 4:4).- ложного совместного использования для некоторых других неатомных переменных- > конвейер mis-спекуляции порядка памяти очищается, а также дополнительные пропуски кэша.
- вместо
-
на переменных FP, XOR высокий байт с 0x80, чтобы перевернуть знаковый бит, вызывая магазин-экспедиционных палатках.- время каждой итерации независимо, с чем - то еще тяжелее чем
RDTSC
. например,CPUID
/RDTSC
или функция времени, которая делает системный вызов. Сериализация инструкций по своей сути является недружественной для конвейера.- изменение умножается на константы и делится на их взаимные ("для удобства чтения"). div является медленным и не полностью конвейерным.
- векторизовать multiply/sqrt с AVX (SIMD), но не использовать
vzeroupper
перед вызовами скалярной математической библиотекиexp()
иlog()
функции, вызывая AVX SSE transition stalls.- сохраните выходные данные RNG в связанном списке или в массивах, которые вы пересекаете не по порядку. То же самое для результата каждой итерации, и сумма в конце.
также рассматривается в этом ответе, но исключается из резюме: предложения, которые были бы столь же медленными на непроверенном процессоре, или которые, похоже, не оправданы даже с дьявольской некомпетентностью. например, многие идеи gimp-компилятора, которые производят очевидно хуже АСМ.
многопоточность плохо
возможно, используйте OpenMP для многопоточных циклов с очень небольшим количеством итераций, с гораздо большими накладными расходами, чем увеличение скорости. Ваш код Монте-Карло имеет достаточно параллелизма, чтобы на самом деле получить ускорение, хотя, esp. если нам удастся сделать каждую итерацию медленной. (Каждый поток вычисляет частичное
payoff_sum
добавил в конце).#omp parallel
на этом цикле, вероятно, будет оптимизация, а не a пессимизм.многопоточность, но заставить оба потока использовать один и тот же счетчик циклов (с
atomic
инкременты, поэтому общее количество итераций правильно). это кажется дьявольски логично. Это означает использованиеstatic
переменная как счетчик циклов. Это оправдывает использованиеatomic
для счетчиков циклов, и создает реальное cache-line ping-ponging (пока потоки не работают на одном физическом ядре с hyperthreading; это может быть не так как медленно). Во всяком случае, это много медленнее, чем не оспариваемый случай дляlock inc
. Иlock cmpxchg8b
для атомарного увеличения утвержденногоuint64_t
на 32-битной системе придется повторить попытку в цикле вместо того, чтобы аппаратный арбитраж атомарныйinc
.также создать ложного совместного использования, где несколько потоков хранят свои личные данные (например, состояние RNG) в разных байтах одной и той же строки кэша. (Intel tutorial about это, в том числе perf счетчики, чтобы посмотреть). в этом есть специфический для микроархитектуры аспект: процессоры Intel спекулируют на неправильном заказе памяти не происходит, и есть память-заказать машину-очистить perf событие, чтобы обнаружить это, по крайней мере на P4. Штраф может быть не таким большим на Хасвелле. Как указывает эта ссылка, a
lock
инструкция ed предполагает, что это произойдет, избегая неправильных спекуляций. Нормальная нагрузка предполагает, что другие ядра не будут аннулировать строку кэша между тем, когда выполняется загрузка и когда она удаляется в программном порядке (если вы используетеpause
). Истинный обмен безlock
инструкции ed, как правило, ошибка. Было бы интересно сравнить неатомный общий счетчик циклов с атомарным случаем. Чтобы действительно пессимизировать, сохраните общий счетчик атомарного цикла и вызовите ложное совместное использование в той же или другой строке кэша для некоторой другой переменной.
случайные уарч-конкретные идеи:
если вы можете представить любые непредсказуемые ветви, что существенно пессимизирует код. Современные процессоры x86 имеют довольно длинные конвейеры, поэтому неверный прогноз стоит ~15 циклов (при запуске из кэша uop).
цепочек зависимостей:
я думаю, что это была одна из предполагаемых частей задания.
уничтожьте способность процессора использовать параллелизм на уровне инструкций, выбрав порядок операций, который имеет одну длинную цепочку зависимостей вместо нескольких коротких цепочек зависимостей. Компиляторы не могут изменять порядок операций для вычислений FP, если вы не используете
-ffast-math
, потому что это может изменить результаты (как описано ниже).чтобы действительно сделать это эффективным, увеличьте длину цепи зависимостей с петлей. Однако ничто не выскакивает так очевидно: циклы, как написано, имеют очень короткие циклические цепочки зависимостей: просто FP добавлять. (3 цикла). Несколько итераций могут иметь свои вычисления в полете сразу, потому что они могут начать задолго до
payoff_sum +=
в конце предыдущей итерации. (log()
иexp
принять много инструкций, но не намного больше, чем Хасвел вышел из строя окно для поиска параллелизма: размер Роб=192 плавленый-домен uops, и размер планировщик=60 несмешанный-домен uops. Как только выполнение текущей итерации продвинется достаточно далеко, чтобы освободить место для инструкций от следующая итерация для выпуска, любые ее части, которые имеют свои входные данные готовыми (т. е. независимая/отдельная цепочка dep), могут начать выполняться, когда более старые инструкции оставляют исполнительные блоки свободными (например, потому что они являются узким местом по задержке, а не пропускной способности.).состояние RNG почти наверняка будет более длинной цепочкой зависимостей с циклом, чем
addps
.
используйте более медленные / дополнительные операции FP (esp. еще деление):
разделить на 2.0 вместо умножение на 0.5, и так далее. ФП размножаются активно задействованы в конструкции процессоров Intel, а также имеет на пропускную способность 0.5 C на Haswell и позже. FP
divsd
/divpd
только частично переданные. (Хотя Skylake имеет впечатляющую пропускную способность на 4C дляdivpd xmm
, С задержкой 13-14c, vs вообще не конвейеризован на Nehalem (7-22c)).The
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 math ops take ~120 дополнительных циклов ловушка для микрокода, когда работа на двух обычных чисел производит Донормила. Увидеть палочек agner туман microarch PDF для точных цифр и деталей. Это маловероятно, так как у вас есть много умножений, поэтому масштабный коэффициент будет возведен в квадрат и уменьшится до 0.0. Я не вижу никакого способа оправдывать необходимое масштабирование нужно некомпетентностью (даже дьявольской), только умышленной злобой.
если вы можете использовать встроенные функции (
<immintrin.h>
)использовать
movnti
чтобы выселить ваши данные из кэша. Дьявольский: это новый и слабо упорядоченный, так что должен позволить процессору работать быстрее, верно? Или см. Этот связанный вопрос для случая, когда кто-то был в опасности сделать именно это (для разбросанных записей, где только некоторые из мест были горячими).clflush
вероятно, невозможно без злобы.используйте целочисленные перетасовки между математическими операциями FP, чтобы вызвать задержки обхода.
смешивание инструкций SSE и AVX без надлежащего использования
vzeroupper
вызывает большие киоски в pre-Skylake (и другой штраф в армию). Даже без этого плохая векторизация может быть хуже скалярной (больше циклов, затраченных на перетасовку данных в / из векторов, чем сохраненных при выполнении добавьте/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
чтобы настроить следующий векторный элемент в качестве arg дляexp
также будет глохнуть. А потомexp()
снова остановится, когда он запускает инструкцию SSE. именно это и произошло в этом вопрос, вызывая замедление в 10 раз. (Спасибо @ZBoson).см. также эксперименты Натана Курца с математической библиотекой Intel против glibc для этого кода. Будущее glibc придет с векторизовать реализации
exp()
и так далее.
если таргетинг pre-IvB, или esp. Nehalem, попробуйте заставить gcc вызвать частичные регистровые киоски с 16-битными или 8-битными операциями, за которыми следуют 32-битные или 64-битные операции. В большинстве случаев ССЗ будет использовать
movzx
после 8-или 16-bit работа, но вот случай, когда gcc изменяетah
а потом читаетax
с (встроенным) asm:
с помощью (встроенного) asm вы можете разбить кэш uop: кусок кода 32B, который не помещается в три строки кэша 6uop, заставляет переключаться с кэша uop на декодеры. Некомпетентный
ALIGN
через много однобайтовыхnop
s вместо пары длинныхnop
s на цели ветви внутри внутренний цикл может сделать трюк. Или поместите прокладку выравнивания после метки, а не перед ней. :P это имеет значение только в том случае, если интерфейс является узким местом, чего не будет, если нам удастся пессимизировать остальную часть кода.используйте самомодифицирующийся код для запуска очистки конвейера (он же machine-nukes).
LCP stalls из 16-битных инструкций с непосредственными слишком большими, чтобы вписаться в 8 бит, вряд ли будут полезны. Кэш uop на SnB и более поздних версиях означает, что вы платите штраф за декодирование только один раз. На Nehalem (первый i7) он может работать для цикла, который не помещается в буфер цикла 28 uop. gcc иногда генерирует такие инструкции, даже с
-mtune=intel
и когда он мог бы использовать 32-разрядные инструкции.
распространенной идиомой для синхронизации является
CPUID
(сериализовать) тогдаRDTSC
. Время каждой итерации отдельно сCPUID
/RDTSC
чтобы убедиться, чтоRDTSC
не переставлять с ранее инструкции, которые будут замедлять работу много. (В реальной жизни умный способ времени-это время всех итераций вместе, вместо того, чтобы синхронизировать каждую отдельно и добавлять их).
причина много промахов кэша и других замедлений памяти
использовать
union { double d; char a[8]; }
для некоторых из ваших переменных. вызвать стойло переадресации магазина путем выполнения узкого хранилища (или чтения-изменения-записи) только для одного из байтов. (Эта статья Вики также охватывает много других микроархитектурных материалов для очередей загрузки/хранения). например,переверните знакdouble
использование XOR 0x80 только на высоком байте, вместо-
оператора. Дьявольски некомпетентным разработчиком, может быть, слышали, что ФП работает медленнее, чем целое, и таким образом постараться сделать как можно больше, используя целочисленные операции. (Очень хороший компилятор, ориентированный на FP math в регистрах SSE, может скомпилировать это вxorps
с константой в другом xmm регистр, но единственный способ, которым это не страшно для x87, - это если компилятор понимает, что он отрицает значение и заменяет следующее добавление вычитанием.)
использовать
volatile
если вы компилируете с-O3
а не черезstd::atomic
, чтобы заставить компилятор фактически хранить / перезагружать все место. Глобальные переменные (вместо локальных) также заставят некоторые магазины/перезагрузки, но слабый порядок модели памяти C++ не требует компилятора разлив/перезагрузка в память все время.замените локальные vars членами большой структуры, чтобы вы могли управлять макетом памяти.
используйте массивы в структуре для заполнения (и хранения случайных чисел, чтобы оправдать их существование).
выберите макет памяти так все идет в другую строку в том же" наборе " в кэше L1. Это только 8-полосная ассоциативная, т. е. каждый набор имеет 8 "путей". Строки кэша являются 64B.
еще лучше, положите вещи точно 4096b друг от друга, так как нагрузки имеют ложную зависимость от магазинов на разных страницах, но с одинаковым смещением внутри страницы. Агрессивные неупорядоченные процессоры используют неоднозначность памяти, чтобы выяснить, когда нагрузки и магазины могут быть переупорядочены без изменения результатов, и реализация Intel имеет ложные срабатывания, которые предотвращают раннее начало загрузки. Вероятно, они только проверяют биты ниже смещения страницы, поэтому проверка может начаться до того, как TLB переведет старшие биты с виртуальной страницы на физическую страницу. А также руководство Агнера, см. ответ от Стивена канон, а также раздел в конце ответа @Krazy Glew на тот же вопрос. (Энди Глю был одним из архитекторов оригинальной микроархитектуры Intel P6.)
использовать
__attribute__((packed))
чтобы вы могли неправильно выровнять переменные, чтобы они охватывали границы кэш-линии или даже страницы. (Так что груз одинdouble
требуются данные из двух кэш-строках). Разрегулированные нагрузки без штрафа в любой Intel i7 с уарч, кроме как при пересечении линии кэша и строк на странице. кэш-линии расщепления по-прежнему занимают дополнительные циклы. Skylake резко снижает штраф за разделенные загрузки страниц,от 100 до 5 циклов. (Раздел 2.1.3). Возможно, это связано с возможностью делать две страницы параллельно.раздел страницы на
atomic<uint64_t>
должно быть в самом худшем случае, esp. если это 5 байты на одной странице и 3 байта на другой странице, или что-нибудь другое, кроме 4:4. Даже разбиения по середине более эффективны для разбиений кэш-линии с векторами 16B на некоторых uarches, IIRC. Положите все вalignas(4096) struct __attribute((packed))
(для экономии места, конечно), включая массив для хранения результатов ГСЧ. Достигните рассогласования с помощьюuint8_t
илиuint16_t
за что-то перед прилавком.если вы можете заставить компилятор использовать индексированные режимы адресации, это будет поражение uop micro-fusion. Может быть, с помощью
#define
s для замены простых скалярных переменных наmy_data[constant]
.если вы можете ввести дополнительный уровень косвенности, поэтому адреса загрузки/хранения не известны заранее, это может еще больше пессимизировать.
пересекать массивы в несмежном порядке
я думаю, что мы можем придумать некомпетентное обоснование для введения массива в первую очередь: это позволяет нам отделить генерацию случайных чисел от использование случайных чисел. Результаты каждой итерации также могут быть сохранены в массиве, который будет суммирован позже (с более дьявольской некомпетентностью).
для "максимальной случайности" мы могли бы иметь поток, петляющий по случайному массиву, записывая в него новые случайные числа. Поток, потребляющий случайные числа, может генерировать случайный индекс для загрузки случайного числа. (Здесь есть некоторые make-work, но микроархитектурно это помогает для загрузки-адреса должны быть известны заранее, поэтому любая возможная нагрузка задержка может быть решена до того, как загруженные данные необходимы.) Наличие устройства чтения и записи на разных ядрах приведет к очистке конвейера неправильных спекуляций с упорядочением памяти (как обсуждалось ранее для случая ложного совместного использования).
для максимальной пессимизации, цикл над массивом с шагом 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, ...этот это то, что вы получите для доступа к 2D массиву, как
double rng_array[MAX_ROWS][512]
в неправильном порядке (цикл по строкам, а не по столбцам внутри строки во внутреннем цикле, как предложено @JesperJuhl). Если дьявольская некомпетентность может оправдать 2D-массив с такими размерами, Садовая разновидность реальной некомпетентности легко оправдывает цикл с неправильным шаблоном доступа. Это происходит в реальном коде в реальной жизни.при необходимости отрегулируйте границы цикла, чтобы использовать много разных страниц вместо повторного использования же несколько страниц, если массив не такой большой. Аппаратная предварительная выборка не работает (а также/вообще) на страницах. Prefetcher может отслеживать один прямой и один обратный поток внутри каждой страницы (что и происходит здесь), но будет действовать только в том случае, если пропускная способность памяти еще не насыщена без предварительной выборки.
это также создаст много промахов TLB, если страницы не будут объединены в hugepage (Linux делает это оппортунистически для анонимных (не с файловой поддержкой) распределения вроде
malloc
/new
чтобы использоватьmmap(MAP_ANONYMOUS)
).вместо массива для хранения списка результатов можно использовать список ссылок. Тогда для каждой итерации потребуется нагрузка, преследующая указатель (необработанная истинная опасность зависимости для адреса нагрузки следующей нагрузки). С плохим распределителем вы можете разбросать узлы списка по памяти, победив кэш. С дьявольски некомпетентным распределителем он мог бы поместить каждый узел в начало собственной страницы. (например, выделить с
mmap(MAP_ANONYMOUS)
напрямую, без разбиения страниц или отслеживания размеров объектов для правильной поддержкиfree
).
они не являются специфичными для микроархитектуры и имеют мало общего с конвейером (большинство из них также будет замедляться на непроверенном процессоре).
несколько не по теме: заставить компилятор генерировать худший код / сделать больше работы:
Используйте C++11
std::atomic<int>
иstd::atomic<double>
для самых пессимальный код. В MFENCEs иlock
инструкции ed довольно медленны даже без конкуренции из другого потока.
-m32
сделает более медленный код, потому что код x87 будет хуже, чем код SSE2. Стек на основе 32-битного соглашения о вызовах принимает больше инструкций и передает даже FP args в стеке таким функциям, какexp()
.