Как проверить размер строки кэша процессора с кодом C ++? - PullRequest
2 голосов
/ 05 октября 2019

Я прочитал статью из блога Игоря . В статье сказано:

... современные процессоры не обращаются к памяти побайтно. Вместо этого они извлекают память кусками (обычно) 64 байта, называемыми строками кэша. Когда вы читаете определенную ячейку памяти, вся строка кэша извлекается из основной памяти в кеш. И доступ к другим значениям из той же строки кэша обходится дешево!

В статье также приведен код на c # для проверки вышеприведенного вывода:

int[] arr = new int[64 * 1024 * 1024];

// Loop 1 (step = 1)
for (int i = 0; i < arr.Length; i++) arr[i] *= 3;

// Loop 2 (step = 16)
for (int i = 0; i < arr.Length; i += 16) arr[i] *= 3;

Два цикла for занимают околов то же время: 80 и 78 мс соответственно на машине Игоря, поэтому проверяется механизм строки кэша.

И затем я отсылаю вышеупомянутую идею к реализации версии c ++ для проверки размера строки кэша следующим образом:

#include "stdafx.h"
#include <iostream>
#include <chrono>
#include <math.h>
using namespace std::chrono;

const int total_buff_count = 16;
const int buff_size = 32 * 1024 * 1024;

int testCacheHit(int * pBuffer, int size, int step)
{
    int result = 0;
    for (int i = 0; i < size;) {
        result += pBuffer[i];
        i += step;
    }

    return result;
}

int main()
{
    int * pBuffer = new int[buff_size*total_buff_count];

    for (int i = 0; i < total_buff_count; ++i) {
        int step = (int)pow(2, i);

        auto start = std::chrono::system_clock::now();
        volatile int result = testCacheHit(pBuffer + buff_size*i, buff_size, step);
        auto end = std::chrono::system_clock::now();

        std::chrono::duration<double> elapsed_seconds = end - start;
        std::cout << "step: " << step << ", elapsed time: " << elapsed_seconds.count() * 1000 << "ms\n";
    }

    delete[] pBuffer;
}

Но мой результат теста совершенно не такой, как в статье Игоря. Если шаг равен 1, тогда временные затраты составляют около 114 мс;Если шаг равен 16, то стоимость времени составляет около 78 мс. Тестовое приложение построено с конфигурацией выпуска, на моей машине 32 ГБ памяти, а процессор - Intel Xeon E5 2420 v2 2.2G;результат следующий. C++ test result

Интересным выводом является то, что затраты времени значительно уменьшились, когда шаг 2 и шаг 2048. Мой вопрос заключается в том, как объяснить разрыв, когда шаг 2 и шаг 2048в моем тесте? И как проверить размер строки кэша процессора с кодом C ++? Почему мой результат полностью отличается от результата Игоря? Спасибо.

Мое собственное объяснение первого вопроса состоит в том, что временная стоимость кода состоит из двух частей: одна - «чтение / запись памяти», которая содержит стоимость чтения / записи памяти, другая - «другие затраты». который содержит для цикла и расчета стоимости. Если шаг равен 2, то стоимость «чтения / записи в память» почти не изменяется (из-за строки кэша), но стоимость вычислений и циклов сократилась вдвое, поэтому мы видим очевидный разрыв. И я предполагаю, что строка кэша на моем процессоре составляет 4096 байтов (1024 * 4 байта), а не 64 байта, поэтому мы получили еще один разрыв, когда шаг 2048. Но это только мое предположение. Спасибо за любую помощь от ваших парней.

Ответы [ 3 ]

2 голосов
/ 05 октября 2019

Падение между 1024 и 2048

Обратите внимание, что вы используете неинициализированный массив . Это в основном означает, что

int * pBuffer = new int[buff_size*total_buff_count];

не заставляет вашу программу фактически запрашивать какую-либо физическую память. Вместо этого зарезервировано только некоторое виртуальное адресное пространство.

Затем, когда вы сначала касаетесь какого-либо элемента массива, запускается сбой страницы , и ОС отображает страницу в физическую память. Это относительно медленная операция, которая может существенно повлиять на ваш эксперимент. Поскольку размер страницы в вашей системе, вероятно, 4 кБ , он может содержать 1024 4-байтовых целых чисел . Когда вы выбираете 2048 step, тогда только каждая вторая страница фактически доступна , и время выполнения пропорционально уменьшается.

Вы можете избежать негативного эффекта этого механизма, предварительно «прикоснувшись» к памяти:

int * pBuffer = new int[buff_size*total_buff_count]{};

Когда я попробовал это, я получил почти линейное уменьшение времени между 64 и 8192 шагами. размеры.

Пропуск между 1 и 2

Размер строки кэша в вашей системе определенно не 2048 байт, скорее всего, 64 байта (как правило, он может иметь разные значения и даже разные значения дляразличные уровни кэша).

Что касается первой части, то для step, равным 1, просто значительно больше арифметических операций (добавление элементов массива и приращений i),

Отличия от эксперимента Игоря

Мы можем только догадываться о том, почему эксперимент Игоря давал практически одинаковые времена в обоих случаях. Я бы предположил, что время выполнения арифметики там ничтожно , так как задействуется только приращение счетчика одного цикла, и он записывает в массив, что требует дополнительной передачи кэшированных строк обратно в память . (Можно сказать, что соотношение byte / op намного выше, чем в вашем эксперименте.)

2 голосов
/ 05 октября 2019

Как проверить размер строки кэша ЦП с кодом C ++?

В C ++ 17 есть std::hardware_destructive_interference_size, который должен обеспечивать наименьший размер строки кэша. Обратите внимание, что это значение времени компиляции, и компилятор полагается на ваши данные о том, на какую машину ориентирован. При нацеливании на всю архитектуру число может быть неточным.

1 голос
/ 05 октября 2019

Как проверить размер строки кэша ЦП с кодом C ++?

Вы не можете надежно.

И вам следует написать portable C ++ код. Прочитайте n3337 .

Представьте, что вы не включили оптимизации компилятора в своем компиляторе C ++. И представьте, что вы запускаете свой компилятор C ++ в каком-то эмуляторе (например, this ).

В Linux, в частности, вы можете проанализировать псевдо-файл /proc/cpuinfo и получить из него размер строки кэша ЦП. .

Например:

% head -20 /proc/cpuinfo
processor   : 0
vendor_id   : AuthenticAMD
cpu family  : 23
model       : 8
model name  : AMD Ryzen Threadripper 2970WX 24-Core Processor
stepping    : 2
microcode   : 0x800820b
cpu MHz     : 1776.031
cache size  : 512 KB
physical id : 0
siblings    : 48
core id     : 0
cpu cores   : 24
apicid      : 0
initial apicid  : 0
fpu     : yes
fpu_exception   : yes
cpuid level : 13
wp      : yes
flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid amd_dcm aperfmperf pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb hw_pstate sme ssbd sev ibpb vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx smap clflushopt sha_ni xsaveopt xsavec xgetbv1 xsaves clzero irperf xsaveerptr arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif overflow_recov succor smca

Кстати, существует множество различных организаций и уровней кэширования.

Вы можете представить себе приложение C ++ в Linux, анализирующее вывод /proc/cpuinfo и затем отправляет HTTP-запросы (используя libcurl ) в Интернет, чтобы получить от него больше информации.

См. Также этот ответ.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...