По крайней мере, с типичным настольным процессором вы не можете подробно указывать на использование кеша напрямую. Тем не менее, вы все равно можете написать код, полезный для кэширования. С кодовой стороны это часто означает, что развертки цикла (только для одного очевидного примера) редко используются - он расширяет код, а современный процессор обычно минимизирует накладные расходы на цикл. Обычно вы можете сделать больше на стороне данных, чтобы улучшить локальность ссылок, защитить от ложного обмена (например, два часто используемых фрагмента данных, которые будут пытаться использовать одну и ту же часть кеша, в то время как другие части остаются неиспользованными).
Edit (чтобы сделать некоторые моменты немного более явные):
Типичный процессор имеет целый ряд различных кэшей. Современный настольный процессор обычно имеет как минимум 2 и часто 3 уровня кеша. По (по крайней мере, почти) универсальному соглашению «уровень 1» является кешем «ближе всего» к элементам обработки, а номера идут вверх (уровень 2 следующий, уровень 3 после этого и т. Д.)
В в большинстве случаев (по крайней мере) кеш уровня 1 разделен на две половины: кеш команд и кеш данных (Intel 486 - это единственное исключение, о котором я знаю, с одним кешем для обеих команд и данных - но он настолько устарел, что, вероятно, не заслуживает много размышлений).
В большинстве случаев кеш организован как набор «линий». Содержимое кеша обычно считывается, записывается и отслеживается по одной строке за раз. Другими словами, если ЦП будет использовать данные из любой части строки кэша, эта целая строка кэша будет считываться со следующего более низкого уровня хранения. Кэши, которые ближе к процессору, обычно меньше и имеют меньшие строки кэша.
Эта базовая архитектура приводит к большинству характеристик кеша, которые имеют значение при написании кода. Насколько это возможно, вы хотите что-то прочитать в кеше один раз, сделать все с ним, что собираетесь, а затем перейти к чему-то еще.
Это означает, что при обработке данных обычно лучше читать относительно небольшой объем данных (достаточно маленький, чтобы вписаться в кеш), делать как можно больше обработки этих данных, а затем перейти к следующий фрагмент данных. Алгоритмы, такие как Quicksort, которые быстро ломают большие объемы ввода в постепенно уменьшенные кусочки, делают это более или менее автоматически, поэтому они, как правило, довольно удобны для кеша, почти независимо от точных деталей кеша.
Это также имеет последствия для того, как вы пишете код. Если у вас есть цикл, как:
for i = 0 to whatever
step1(data);
step2(data);
step3(data);
end for
Вы обычно лучше нанизывать, как многие из шагов вместе, как вы можете до суммы, которая будет вписываться в кэше. В тот момент, когда вы переполняете кеш, производительность может/резко упасть.Если код для шага 3 выше было достаточно большим, что она не будет вписываться в кэш, вы, как правило, будет лучше разорвать петлю на две части как это (если это возможно):
for i = 0 to whatever
step1(data);
step2(data);
end for
for i = 0 to whatever
step3(data);
end for
Loop разворачивая является довольно спорным предметом. С одной стороны, это может привести к коду, значительно превосходящему ЦП, уменьшая накладные расходы на выполнение инструкций для самого цикла. В то же время он может (и вообще делает) увеличивать размер кода, поэтому он относительно недружелюбен кэш. Мой собственный опыт заключается в том, что в синтетических тестах, которые имеют тенденцию делать действительно небольшие объемы обработки на действительно больших объемах данных, вы получаете много от разворота цикла. В более практичном коде, где вы, как правило, больше обрабатываете отдельные части данных, вы получаете намного меньше - и переполнение кеша, приводящее к серьезной потере производительности, не является особенно редким.
Кэш данных также ограничен по размеру. Это означает, что вы, как правило, хотите, чтобы ваши данные были упакованы как можно более плотно, чтобы максимально возможное количество данных входило в кеш. Только для одного очевидного примера структура данных, связанная с указателями, должна получить довольно много с точки зрения вычислительной сложности, чтобы компенсировать объем пространства кэша данных, используемого этими указателями. Если вы собираетесь использовать связанную структуру данных, вы, как правило, хотите, по крайней мере, обеспечить, чтобы вы связывали относительно большие части данных.
Во многих случаях, однако, я обнаружил, что трюки, которые я изначально изучил для подгонки данных в незначительные объемы памяти в крошечных процессорах, которые были (в основном) устаревшими на протяжении десятилетий, очень хорошо работают на современных процессорах. Намерение теперь состоит в том, чтобы вставить больше данных в кеш вместо основной памяти, но эффект почти такой же. В довольно многих случаях вы можете думать о инструкциях CPU как о бесплатном, а общая скорость выполнения определяется пропускной способностью кэш-памяти (или основной памяти), поэтому дополнительная обработка для распаковки данных из плотного формата работает в ваша милость. Это особенно актуально, когда вы имеете дело с достаточным количеством данных, которые не все будут вписываться в кеш, и поэтому общая скорость определяется пропускной способностью основной памяти. В этом случае вы можете выполнить команду , чтобы сохранить несколько считываний по памяти и все еще выйти вперед.
Параллельная обработка может усугубить эту проблему. Во многих случаях переписывание кода для параллельной обработки может привести к практически отсутствию производительности или иногда даже к потере производительности. Если общая скорость определяется полосой пропускания от CPU к памяти, наличие большего количества ядер, конкурирующих за эту полосу пропускания, вряд ли принесет пользу (и может нанести существенный вред). В таком случае использование нескольких ядер для повышения скорости часто сводится к тому, чтобы сделать еще больше, чтобы более точно упаковать данные и использовать еще большую вычислительную мощность для распаковки данных, поэтому реальное увеличение скорости связано с уменьшением потребляемой полосы пропускания , а дополнительные ядра просто не теряют времени для распаковки данных из более плотного формата.
Другая проблема с кешем, которая может возникать при параллельном кодировании, заключается в совместном использовании (и ложном обмене) переменных. Если два (или более) ядра необходимо записать в одно и то же место в памяти, линия кэша, содержащая эти данные, может быть переведена туда и обратно между ядрами, чтобы предоставить каждому ядру доступ к совместно используемым данным. Результатом часто является код, который работает медленнее параллельно, чем в серийном (то есть на одном ядре). Существует вариация, называемая «ложным совместным использованием», в которой код на разных ядрах записывается для разделения данных, , но данные для разных ядер заканчиваются в одной и той же строке кэша. Поскольку кеш управляет данными исключительно по всем строкам данных, данные все равно перетасовываются между ядрами, что приводит к точно такой же проблеме.
«Мало достаточно» важно, но так «достаточно близко» и «достаточно близко друг к другу во времени». Кэши могут удерживать только столько, поэтому сделайте это неплохим жестким пакетом, где все, что вам нужно в одно и то же время, физически смежно в тот же момент времени. – RocketRoy