Это может звучать как субъективный вопрос, но я ищу конкретные примеры, с которыми вы могли столкнуться, связанные с этим.
Как сделать код эффективным кешем / дружественным кешем (больше обращений к кешу, как можно меньше пропусков кеша)? С обеих точек зрения кэш данных и программный кеш (кеш инструкций), т. Е. Какие вещи в коде, связанные со структурами данных и конструкциями кода, должны заботиться о том, чтобы сделать его кеширующим эффективным.
Существуют ли какие-либо конкретные структуры данных, которые нужно использовать / избегать, или есть особый способ доступа к членам этой структуры и т. Д., Чтобы сделать кэш кода эффективным.
Существуют ли какие-либо программные конструкции (если for, switch, break, goto, ...), поток кода (для if, if внутри for и т. Д.), Которых следует придерживаться / избегать в этом вопросе?
Я с нетерпением жду возможности услышать индивидуальный опыт, связанный с созданием эффективного кеш-кода в целом. Это может быть любой язык программирования (C, C ++, Assembly, ...), любая аппаратная цель (ARM, Intel, PowerPC, ...), любая ОС (Windows, Linux, Symmbian, ...) и т. Д. ,
Разнообразие поможет лучше понять его глубоко.
источник
Ответы:
Кэш предназначен для уменьшения количества раз, которое ЦП будет останавливать в ожидании выполнения запроса памяти (избегая задержки памяти ), и в качестве второго эффекта, возможно, для уменьшения общего объема данных, которые должны быть переданы (сохранение пропускная способность памяти ).
Методы, позволяющие избежать страданий из-за задержек при извлечении памяти, обычно являются первыми, на которые стоит обратить внимание, а иногда и помогают. Ограниченная пропускная способность памяти также является ограничивающим фактором, особенно для многоядерных и многопоточных приложений, где многие потоки хотят использовать шину памяти. Другой набор методов помогает решить последнюю проблему.
Улучшение пространственной локальности означает, что вы гарантируете, что каждая строка кэша используется полностью после ее сопоставления с кэшем. Когда мы рассмотрели различные стандартные тесты, мы увидели, что удивительно большая часть из них не использует 100% извлеченных строк кэша до того, как строки кэша будут удалены.
Улучшение использования строк кэша помогает в трех отношениях:
Общие методы:
Также следует отметить, что существуют другие способы скрыть задержку памяти, кроме использования кешей.
Современные процессоры часто имеют один или несколько аппаратных предварительных загрузчиков . Они тренируются по промахам в тайнике и пытаются выявить закономерности. Например, после нескольких пропусков в последующих строках кеша, средство предварительной выборки hw начнет извлекать строки кеша в кеш, предвидя потребности приложения. Если у вас есть обычный шаблон доступа, аппаратный предварительный выборщик обычно делает очень хорошую работу. И если ваша программа не отображает обычные шаблоны доступа, вы можете улучшить ситуацию, добавив инструкции предварительной выборки самостоятельно.
Перегруппировав инструкции таким образом, чтобы те, которые всегда пропускали в кеше, находились близко друг к другу, ЦП иногда может перекрывать эти выборки, так что приложение выдерживает только одно попадание задержки ( параллелизм на уровне памяти ).
Чтобы уменьшить общее давление на шину памяти, вы должны начать работу с так называемой временной локализацией . Это означает, что вы должны повторно использовать данные, пока они еще не были удалены из кэша.
Объединение циклов, которые касаются одних и тех же данных (объединение циклов ), и использование методов перезаписи, известных как разбиение на листы или блокировка всех, стремятся избежать этих дополнительных выборок памяти.
Несмотря на то, что для этого упражнения по переписыванию есть несколько практических правил, вам, как правило, необходимо тщательно учитывать зависимости данных, переносимых в цикле, чтобы не влиять на семантику программы.
Это то, что действительно окупается в многоядерном мире, где вы, как правило, не увидите значительных улучшений пропускной способности после добавления второго потока.
источник
Я не могу поверить, что нет больше ответов на это. В любом случае, одним из классических примеров является итерация многомерного массива «наизнанку»:
Причина в том, что кэш неэффективен, потому что современные процессоры будут загружать строку кеша с «близкими» адресами памяти из основной памяти, когда вы обращаетесь к одному адресу памяти. Мы выполняем итерацию по «j» (внешним) строкам в массиве во внутреннем цикле, поэтому для каждой поездки по внутреннему циклу строка кэша будет сбрасываться и загружаться строкой адресов, которые находятся рядом с [ j] [i] запись. Если это изменено на эквивалент:
Это будет работать намного быстрее.
источник
Основные правила на самом деле довольно просты. Трудно понять, как они применяются к вашему коду.
Кеш работает по двум принципам: временная локальность и пространственная локальность. Первая идея заключается в том, что если вы недавно использовали определенную порцию данных, вам, вероятно, скоро понадобится это снова. Последнее означает, что если вы недавно использовали данные по адресу X, вам, вероятно, скоро понадобится адрес X + 1.
Кэш пытается приспособиться к этому, запоминая последние использованные порции данных. Он работает со строками кэша, обычно размером 128 байт или около того, поэтому, даже если вам нужен только один байт, вся содержащая его строка кэша вытягивается в кэш. Так что если вам понадобится следующий байт, он уже будет в кеше.
А это значит, что вы всегда захотите, чтобы ваш собственный код максимально использовал эти две формы локальности. Не перепрыгивайте всю память. Сделайте как можно больше работы на одной маленькой области, а затем переходите к следующей и делайте там столько работы, сколько сможете.
Простой пример - обход 2D-массива, который показал ответ 1800 года. Если вы просматриваете его по очереди, вы читаете память последовательно. Если вы сделаете это по столбцам, вы прочитаете одну запись, затем перейдете в совершенно другое место (начало следующей строки), прочитаете одну запись и снова прыгнете. И когда вы наконец вернетесь к первому ряду, он больше не будет в кеше.
То же самое относится и к коду. Переходы или переходы означают менее эффективное использование кэша (потому что вы не читаете инструкции последовательно, а переходите на другой адрес). Конечно, небольшие if-операторы, вероятно, ничего не изменят (вы пропускаете всего несколько байтов, поэтому вы все равно окажетесь в кэшированной области), но вызовы функций обычно подразумевают, что вы переходите к совершенно другому адрес, который не может быть кэширован. Если только это не было названо недавно.
Использование кеша инструкций, как правило, представляет собой гораздо меньшую проблему. Что вам обычно нужно беспокоиться, так это кеш данных.
В структуре или классе все члены располагаются смежно, и это хорошо. В массиве все записи также располагаются смежно. В связанных списках каждый узел размещается в совершенно другом месте, что плохо. Обычно указатели указывают на несвязанные адреса, что, вероятно, приведет к потере кэша, если вы разыменуете его.
И если вы хотите использовать несколько ядер, это может стать действительно интересным, поскольку обычно только один ЦП может иметь любой данный адрес в своем кеше L1 одновременно. Таким образом, если оба ядра постоянно обращаются к одному и тому же адресу, это приведет к постоянным ошибкам в кэше, так как они борются за адрес.
источник
Я рекомендую прочитать статью из 9 частей Что должен знать каждый программист об памяти Ульриха Дреппера, если вы заинтересованы в том, как взаимодействуют память и программное обеспечение. Он также доступен в виде 104-страничного PDF .
Разделы, особенно относящиеся к этому вопросу, могут быть частью 2 (кэши ЦП) и частью 5 (Что могут сделать программисты - оптимизация кэша).
источник
Помимо шаблонов доступа к данным, основным фактором в кеширующем коде является размер данных . Чем меньше данных, тем больше их помещается в кэш.
Это в основном является фактором с выравниванием памяти структур данных. «Обычная» мудрость гласит, что структуры данных должны быть выровнены по границам слов, потому что ЦП может получить доступ только к целым словам, и если слово содержит более одного значения, вы должны выполнить дополнительную работу (чтение-изменение-запись вместо простой записи) , Но кеши могут полностью опровергнуть этот аргумент.
Точно так же логический массив Java использует целый байт для каждого значения, чтобы позволить работать с отдельными значениями напрямую. Вы можете уменьшить размер данных в 8 раз, если используете фактические биты, но тогда доступ к отдельным значениям становится намного более сложным, требуя операций по сдвигу битов и маски (
BitSet
класс делает это за вас). Однако из-за эффектов кэширования это может быть значительно быстрее, чем использование логического [], когда массив большой. IIRC I однажды таким образом добился ускорения в 2 или 3 раза.источник
Наиболее эффективной структурой данных для кэша является массив. Кэши работают лучше всего, если ваша структура данных размещена последовательно, поскольку процессоры считывают целые строки кэша (обычно 32 байта или более) сразу из основной памяти.
Любой алгоритм, который обращается к памяти в случайном порядке, перебирает кэши, потому что ему всегда нужны новые строки кэша для размещения в произвольно доступной памяти. С другой стороны, алгоритм, который запускается последовательно через массив, лучше, потому что:
Это дает процессору возможность опережать чтение, например, умозрительно помещать больше памяти в кеш, к которому будет обращаться позже. Это упреждающее чтение дает огромный прирост производительности.
Выполнение замкнутого цикла в большом массиве также позволяет процессору кэшировать код, выполняемый в цикле, и в большинстве случаев позволяет выполнять алгоритм полностью из кэш-памяти, не блокируя доступ к внешней памяти.
источник
Один пример, который я видел в игровом движке, - это перемещение данных из объектов в их собственные массивы. К игровому объекту, который подвергался физике, также может быть прикреплено много других данных. Но во время цикла обновления физики все, что беспокоило движок, это данные о положении, скорости, массе, ограничительной рамке и т. Д. Таким образом, все это было помещено в собственные массивы и максимально оптимизировано для SSE.
Поэтому во время цикла физики физические данные обрабатывались в порядке массива с использованием векторной математики. Игровые объекты использовали свой идентификатор объекта в качестве индекса в различных массивах. Это был не указатель, потому что указатели могли стать недействительными, если нужно было переместить массивы.
Во многих случаях это нарушало шаблоны объектно-ориентированного проектирования, но значительно ускоряло работу кода, помещая данные близко друг к другу, которые необходимо было обрабатывать в тех же циклах.
Этот пример, вероятно, устарел, потому что я ожидаю, что в большинстве современных игр используется встроенный физический движок, такой как Havok.
источник
Только один пост коснулся этого, но возникает большая проблема при обмене данными между процессами. Вы хотите избежать нескольких процессов, пытающихся изменить одну и ту же строку кэша одновременно. Здесь нужно обратить внимание на «ложное» совместное использование, когда две смежные структуры данных совместно используют строку кэша, а изменение одной делает недействительной строку кэша для другой. Это может привести к тому, что строки кэша будут излишне перемещаться вперед и назад между процессорными кэшами, разделяющими данные в многопроцессорной системе. Чтобы избежать этого, нужно выровнять и дополнить структуры данных, чтобы поместить их в разные строки.
источник
Замечание к «классическому примеру» пользователя 1800 ИНФОРМАЦИЯ (слишком долго для комментария)
Я хотел проверить разницу во времени для двух порядков итераций («внешний» и «внутренний»), поэтому я провел простой эксперимент с большим 2D-массивом:
и второй случай с
for
замененными петлями.Более медленная версия («x first») была 0,88 с, а более быстрая - 0,06 с. Это сила кеширования :)
Я использовал
gcc -O2
и до сих пор петли не были оптимизированы. Комментарий Рикардо о том, что «большинство современных компиляторов могут сами в этом разобраться», не имеет местаисточник
Я могу ответить (2), сказав, что в мире C ++ связанные списки могут легко уничтожить кэш процессора. Массивы являются лучшим решением, где это возможно. Нет опыта в том, применимо ли это к другим языкам, но легко представить, что возникнут те же проблемы.
источник
Кэш расположен в «строках кэша», и (реальная) память считывается и записывается в виде блоков такого размера.
Следовательно, структуры данных, содержащиеся в одной строке кэша, более эффективны.
Аналогично, алгоритмы, которые обращаются к смежным блокам памяти, будут более эффективными, чем алгоритмы, которые перемещаются по памяти в случайном порядке.
К сожалению, размер строки кэша сильно различается между процессорами, поэтому невозможно гарантировать, что структура данных, оптимальная на одном процессоре, будет эффективна на любом другом.
источник
Спрашивать, как сделать код, эффективно кешировать, дружественным кешу, и большинство других вопросов - это обычно спрашивать, как оптимизировать программу, потому что кеш имеет такое огромное влияние на производительность, что любая оптимизированная программа - это кеш эффективный кеш дружественный
Предлагаю почитать про Оптимизацию, на этом сайте есть несколько хороших ответов. С точки зрения книг, я рекомендую « Компьютерные системы: перспектива программиста», в которой есть небольшой текст о правильном использовании кэша.
(Кстати, как плохо, как может быть из-за кеша, хуже - если программа выгружается с жесткого диска ...)
источник
Было получено много ответов на общие советы, такие как выбор структуры данных, шаблон доступа и т. Д. Здесь я хотел бы добавить еще один шаблон разработки кода, называемый программным конвейером, который использует активное управление кэшем.
Идея состоит в том, чтобы позаимствовать другие методы конвейерной обработки, например конвейерную обработку команд процессора.
Этот тип шаблона лучше всего подходит для процедур, которые
Давайте рассмотрим простой случай, когда есть только одна подпроцедура. Обычно код будет выглядеть так:
Чтобы повысить производительность, вам может потребоваться передать несколько входов в функцию в пакете, чтобы амортизировать накладные расходы на вызовы функций, а также увеличивать локальность кэша кода.
Однако, как было сказано ранее, если выполнение шага примерно совпадает со временем доступа к ОЗУ, вы можете дополнительно улучшить код до чего-то вроде этого:
Поток выполнения будет выглядеть так:
Может потребоваться больше шагов, тогда вы можете разработать многоступенчатый конвейер, если время выполнения шагов и время ожидания доступа к памяти совпадают, и вы будете испытывать небольшие потери в кеше кода / данных. Однако этот процесс должен быть настроен на множество экспериментов, чтобы определить правильную группировку шагов и время предварительной выборки. Из-за его требуемых усилий он видит больше принятия в обработке потока данных / потока высокой производительности. Хороший пример производственного кода можно найти в проекте конвейера очереди QoS DPDK: http://dpdk.org/doc/guides/prog_guide/qos_framework.html Глава 21.2.4.3. Постановка трубопровода.
Более подробную информацию можно найти:
https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and
http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf
источник
Напишите вашу программу, чтобы взять минимальный размер. Вот почему не всегда хорошая идея использовать оптимизацию -O3 для GCC. Это занимает больший размер. Часто -Os так же хорошо, как -O2. Хотя все зависит от используемого процессора. YMMV.
Работайте с небольшими порциями данных одновременно. Вот почему менее эффективные алгоритмы сортировки могут работать быстрее, чем быстрая сортировка, если набор данных большой. Найдите способы разбить ваши большие наборы данных на более мелкие. Другие предложили это.
Чтобы помочь вам лучше использовать временную / пространственную локальность команд, вы можете изучить, как ваш код преобразуется в сборку. Например:
Два цикла создают разные коды, даже если они просто анализируют массив. В любом случае, ваш вопрос очень специфичен для конкретной архитектуры. Таким образом, единственный способ строго контролировать использование кэша - это понять, как работает оборудование, и оптимизировать код для него.
источник
Помимо выравнивания вашей структуры и полей, если ваша структура, если выделена куча, вы можете использовать распределители, которые поддерживают выравниваемые выделения; как _aligned_malloc (sizeof (DATA), SYSTEM_CACHE_LINE_SIZE); иначе у вас может быть случайное ложное разделение; помните, что в Windows куча по умолчанию имеет 16-байтовое выравнивание.
источник