Код дизайна для установки в кеш процессора?
При написании симуляций мой приятель говорит, что ему нравится пытаться написать программу, достаточно маленькую, чтобы вписаться в кеш. Это имеет какой-то реальный смысл? Я понимаю, что кеш быстрее, чем оперативная память и основная память. Можно ли указать, что вы хотите, чтобы программа запускалась из кеша или, по крайней мере, загружала переменные в кеш? Мы пишем симуляции, поэтому любой выигрыш в производительности и оптимизации является огромным преимуществом.
Если вы знаете какие-либо хорошие ссылки, объясняющие кэширование CPU, тогда укажите мне в этом направлении.
Ответы
Ответ 1
По крайней мере, с типичным настольным процессором вы не можете напрямую указывать на использование кеша напрямую. Тем не менее, вы все равно можете написать код, полезный для кэширования. С кодовой стороны это часто означает, что развертки цикла (только для одного очевидного примера) редко используются - он расширяет код, а современный процессор обычно минимизирует накладные расходы на цикл. Обычно вы можете сделать больше на стороне данных, чтобы улучшить локальность ссылок, защитить от ложного обмена (например, две часто используемые части данных, которые будут пытаться использовать одну и ту же часть кеша, в то время как другие части остаются неиспользованными).
Изменить (чтобы сделать несколько точек более явным):
Типичный процессор имеет несколько разных кешей. Современный настольный процессор обычно имеет как минимум 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
Развертка цикла - довольно спорный предмет. С одной стороны, это может привести к тому, что код будет намного более удобным для процессора, что уменьшит накладные расходы на выполнение инструкций для самого цикла. В то же время он может (и вообще делает) увеличивать размер кода, поэтому он относительно кэширует недружественный. Мой собственный опыт заключается в том, что в синтетических тестах, которые имеют тенденцию делать действительно небольшие объемы обработки на действительно больших объемах данных, вы получаете много от разворота цикла. В более практичном коде, где вы, как правило, больше обрабатываете отдельные части данных, вы получаете намного меньше - и переполнение кеша, приводящее к серьезной потере производительности, не является особенно редким.
Кэш данных также ограничен по размеру. Это означает, что вы, как правило, хотите, чтобы ваши данные были упакованы как можно более плотно, чтобы максимально возможное количество данных входило в кеш. Для одного очевидного примера структура данных, связанная вместе с указателями, должна получить довольно много с точки зрения вычислительной сложности, чтобы компенсировать объем пространства кэша данных, используемого этими указателями. Если вы собираетесь использовать связанную структуру данных, вы, как правило, хотите, по крайней мере, обеспечить, чтобы вы связывали относительно большие части данных.
Тем не менее, во многих случаях я обнаружил, что трюки, которые я изначально изучил для подгонки данных в незначительные объемы памяти в крошечных процессорах, которые были (в основном) устаревшими на протяжении десятилетий, хорошо работают на современных процессорах. Намерение теперь состоит в том, чтобы вставить больше данных в кеш вместо основной памяти, но эффект почти такой же. В довольно многих случаях вы можете думать о инструкциях CPU как о бесплатном, а общая скорость выполнения определяется пропускной способностью кэш-памяти (или основной памяти), поэтому дополнительная обработка для распаковки данных из плотного формата работает в ваша милость. Это особенно актуально, когда вы имеете дело с достаточным количеством данных, которые не все будут вписываться в кеш, и поэтому общая скорость определяется пропускной способностью основной памяти. В этом случае вы можете выполнить множество инструкций, чтобы сохранить несколько чтений памяти и все еще выйти вперед.
Параллельная обработка может усугубить эту проблему. Во многих случаях переписывание кода для параллельной обработки может привести к практически отсутствию производительности или иногда даже к потере производительности. Если общая скорость определяется полосой пропускания от CPU к памяти, наличие большего количества ядер, конкурирующих за эту полосу пропускания, вряд ли принесет пользу (и может нанести существенный вред). В таком случае использование нескольких ядер для повышения скорости часто сводится к тому, чтобы сделать еще больше, чтобы более точно упаковать данные и использовать еще большую вычислительную мощность для распаковки данных, поэтому реальное увеличение скорости связано с уменьшением потребляемой полосы пропускания, а дополнительные ядра просто не теряют времени для распаковки данных из более плотного формата.
Другая проблема с кешем, которая может возникнуть при параллельном кодировании, заключается в совместном использовании (и ложном обмене) переменных. Если два (или более) ядра необходимо записать в одно и то же место в памяти, линия кэша, содержащая эти данные, может быть переведена туда и обратно между ядрами, чтобы предоставить каждому ядру доступ к совместно используемым данным. Результатом часто является код, который работает медленнее параллельно, чем в серийном (то есть на одном ядре). Там вариант этого называется "ложным совместным использованием", в котором код на разных ядрах записывает для разделения данных, но данные для разных ядер заканчиваются в одной и той же строке кэша. Поскольку кеш управляет данными только по целым строкам данных, данные все равно перетасовываются между ядрами, что приводит к точно такой же проблеме.
Ответ 2
Здесь ссылка на действительно хорошую статью о оптимизации кэшей/памяти Кристером Эриксеном (о Боге войны I/II/III известности). Это пару лет, но это все еще очень актуально.
Ответ 3
Полезная статья, которая расскажет вам больше, чем вы когда-либо хотели узнать о кешах, - это Что каждый программист должен знать о памяти Ульриха Дреппера. Hennessey очень тщательно его охватывает. Кристер и Майк Актон написали кучу хороших вещей об этом тоже.
Я думаю, вам стоит больше беспокоиться о кеше данных, чем кеш-кеш — по моему опыту, пропуски dcache более часты, более болезненны и более полезны.
Ответ 4
ОБНОВЛЕНИЕ: 1/13/2014
По мнению этого старшего дизайнера чипов, пропуски кеша теперь являются доминирующим фактором в производительности кода, поэтому мы в основном полностью возвращаемся к середине 80-х и быстрым 286 чипам с точки зрения относительной производительности узких мест загрузки, хранения, целого числа арифметика и пропуски кеша.
Крутой курс в современном оборудовании от Cliff Click @Azul
,
,
,
,
.
--- Теперь мы возвращаем вас в вашу регулярно запланированную программу ---
Иногда пример лучше, чем описание того, как что-то делать. В этом духе здесь особенно удачный пример того, как я изменил код, чтобы лучше использовать кеш-кеши. Это было сделано некоторое время назад на процессоре 486, а последний перенесен на процессор Pentium 1-го поколения. Эффект на производительность был аналогичным.
Пример: отображение подстроки
Вот пример метода, который я использовал для подбора данных в кеш-чип, который имеет универсальную утилиту.
У меня был двойной вектор с плавающей точкой, длина которого составляла 1250 элементов, что было кривой эпидемиологии с очень длинными хвостами. "Интересная" часть кривой имела только около 200 уникальных значений, но я не хотел, чтобы 2-сторонний if() тест делал беспорядок в конвейере CPU (таким образом, длинные хвосты, которые могли бы использовать в качестве индексов самые экстремальные значения кода Монте-Карло выплюнули бы), и мне понадобилась логика предсказания ветвления для еще дюжины других условных тестов внутри "горячей точки" в коде.
Я остановился на схеме, где я использовал вектор 8-битных ints в качестве индекса в двойной вектор, который я сократил до 256 элементов. Крошечные ints имели одинаковые значения до 128 перед нулем, а 128 после нуля, поэтому, за исключением средних значений 256, все они указывали либо на первое, либо на последнее значение в двойном векторе.
Это уменьшило требования к хранилищу до 2k для удвоений и 1250 байт для 8-битных индексов. Это сократилось на 10 000 байт до 3,298. Поскольку в этом внутреннем цикле программа потратила 90% или более этого времени, два вектора никогда не выходили из кеша данных 8k. Программа сразу удвоила ее производительность. Этот код попал ~ 100 миллиардов раз в процессе вычисления значения OAS для 1 миллиона миллионов ипотечных кредитов.
Поскольку хвосты кривой были редко затронуты, очень возможно, что на самом деле хранились только средние 200-300 элементов крошечного вектора int, а также 160-240 средних удвоений, представляющих 1/8 тыс. процентов интереса, Это было замечательное увеличение производительности, достигнутое во второй половине дня, в программе, которую я потратил более года на оптимизацию.
Я согласен с Джерри, так же как и мой опыт, что отклонение кода к кэшу команд не так успешно, как оптимизация для кеша данных. Это одна из причин, по которой я думаю, что общие кэши AMD не так полезны, как отдельные тайны данных и команд Intel. IE: вы не хотите, чтобы инструкции загружали кеш, так как это не очень полезно. Частично это объясняется тем, что наборы команд CISC были изначально созданы, чтобы компенсировать огромную разницу между скоростью процессора и памяти, и за исключением аберрации в конце 80-х годов, что почти всегда было правдой.
Другим любимым методом, который я использую для поддержки кэша данных, и явного кэша команд, является использование множества бит-int в определениях структуры и минимально возможных размеров данных в целом. Чтобы замаскировать 4-битный int для хранения месяца в году или 9 бит для хранения дня в году и т.д. И т.д., Требуется, чтобы маски использования процессора маскировали целые числа хостов, которые используют биты, что сокращает данных, эффективно увеличивает размер кеша и шины, но требует больше инструкций. Хотя этот метод создает код, который не работает также на синтетических тестах, на занятых компьютерах, где пользователи и процессы конкурируют за ресурсы, он отлично работает.
Ответ 5
В основном это будет служить заполнителем, пока я не получу время, чтобы сделать эту тему справедливости, но я хотел поделиться тем, что я считаю поистине революционной вехой, - введение специальных инструкций по обработке бит в новом микропроцессоре Intel Hazwell.
Стало болезненно очевидным, когда я написал код здесь, в StackOverflow, чтобы отменить бит в массиве 4096 бит, который через 30+ после введения ПК, микропроцессоры просто не уделяют много внимания или ресурсов битам, и что Надеюсь, это изменится. В частности, мне бы очень хотелось, чтобы во-первых, тип bool стал фактическим битовым типом данных в C/С++, а не до смешного расточительного байта, который он сейчас представляет.
![Hazwell's new Bit Manipulation Instructions]()
ОБНОВЛЕНИЕ: 12/29/2013
Недавно мне приходилось оптимизировать кольцевой буфер, который отслеживает требования 512 пользователей различных ресурсов к системе в миллисекундовой гранулярности. Существует таймер, который запускает каждую миллисекунду, которая добавляет сумму из самых текущих запросов ресурса среза и вычитает 1000-й запрос времени среза, содержащий запросы к ресурсам, которые теперь составляют 1000 миллисекунд.
Голова, хвостовые векторы были рядом друг с другом в памяти, за исключением того, что сначала Голова, а затем Хвост завернута и началась назад в начале массива. Однако срединный (сводный) Срез-сегмент находился в фиксированном, статически распределенном массиве, который не был особенно близок ни к одному из них, и даже не был выделен из кучи.
Размышляя об этом и изучая код, некоторые из них привлекли мое внимание.
-
Требования, которые поступали, были добавлены одновременно в раздел "Заголовок" и "Сводка", рядом друг с другом в смежных строках кода.
-
Когда таймер выстрелил, Хвост вычитался из среза Сводки, и результаты были оставлены в срединном фрагменте, как вы ожидали бы
-
Вторая функция, вызываемая при запуске таймера, продвигает все указатели, обслуживающие кольцо. В частности....
Голова переписала Хвост, тем самым занимая одно и то же место памяти
Новый Tail занял следующие 512 мест памяти или завернул
-
Пользователю требуется больше гибкости в управлении количеством запросов: от 512 до 4098 или, возможно, больше. Я чувствовал, что самый надежный, идиотский способ сделать это состоит в том, чтобы выделить как 1000 квантов времени, так и итоговый срез все вместе как один непрерывный блок памяти, так что НЕВОЗМОЖНО, чтобы срез Сводки заканчивался другой длиной чем другие 1000 срезов времени.
-
Учитывая вышеизложенное, я начал задаваться вопросом, могу ли я получить более высокую производительность, если вместо того, чтобы срез Сводки остался в одном месте, у меня было это "бродить" между Головой и Хвостом, поэтому оно всегда рядом с Главой для добавления новых требований и рядом с Хвостом, когда таймер выстрелил, а значения Хвоста должны были быть вычтены из Сводки.
Я сделал именно это, но затем нашел несколько дополнительных оптимизаций в этом процессе. Я изменил код, который рассчитал скользящее сводку, чтобы он оставил результаты в хвосте, а не срез Сводки. Зачем? Поскольку следующая функция выполняла memcpy(), чтобы переместить срез Summary в память, только что занятую хвостом. (странно, но верно, Хвост ведет голову до конца кольца, когда он обертывается). Оставив результаты суммирования в Tail, мне не пришлось выполнять memcpy(), мне просто нужно было назначить pTail для pSummary.
Аналогичным образом, новая глава заняла теперь устаревшее временное разделение старой ячейки памяти, так что снова я просто назначил pSummary для pHead и обнулял все его значения с помощью memset до нуля.
Пройдя путь до конца кольца (на самом деле барабан шириной 512 дорожек) был хвостом, но мне пришлось сравнить его указатель с постоянным указателем pEndOfRing, чтобы обнаружить это условие. Все остальные указатели могли бы назначить значение указателя вектора перед ним. IE: Мне нужен только условный тест для 1: 3 указателей, чтобы их правильно обернуть.
В исходном проекте использовались байтовые ints для максимизации использования кеша, однако я смог смягчить это ограничение - удовлетворить запросы пользователей обрабатывать более высокие подсчеты ресурсов для каждого пользователя за миллисекунду - использовать беззнаковые шорты и STILL двойную производительность, потому что даже с 3 смежными векторами из 512 неподписанных шорт, кеш-кеш 32-х кеш-памяти L1 может легко удерживать требуемые 3,720 байт, 2/3rds которых были в только что используемых местах. Только когда обложка "Хвост", "Сводка" или "Голова" была 1 из 3, разделенных любым значительным "шагом" в 8-мегабайтном кэше L3.
Общий объем памяти во время выполнения для этого кода составляет менее 2 МБ, поэтому он полностью запускается из кэшей на кристалле, и даже на чипе i7 с 4 ядрами 4 экземпляра этого процесса могут выполняться без какого-либо ухудшения производительность, а общая пропускная способность немного увеличивается с 5 процессами. Это Opus Magnum при использовании кеша.
Ответ 6
Большинство компиляторов C/С++ предпочитают оптимизировать размер, а не "скорость". То есть меньший код обычно выполняется быстрее, чем разворачиваемый код из-за эффектов кеша.
Ответ 7
Если бы я был вами, я бы удостоверился, что знаю, какие части кода являются горячими точками, которые я определяю как
- жесткая петля, не содержащая никаких вызовов функций, потому что если она вызывает любую функцию, тогда ПК будет тратить большую часть своего времени на эту функцию,
- на который приходится значительная часть времени выполнения (например, >= 10%), которую вы можете определить из профилировщика. (Я просто отбираю стек вручную.)
Если у вас есть такая точка доступа, она должна входить в кеш. Я не уверен, как вы это делаете, но я подозреваю, что это автоматическое.