Несоответствие при сравнении двух смежных измерений - PullRequest
8 голосов
/ 18 июня 2019

Я тестировал функцию и вижу, что некоторые итерации выполняются медленнее, чем другие.

После некоторых тестов я попытался сравнить два смежных измерения, и у меня все еще были странные результаты.

код на wandbox .

Для меня важная часть:

using clock = std::chrono::steady_clock;
// ...
for (int i = 0; i < statSize; i++)
{
    auto t1 = clock::now();
    auto t2 = clock::now();
}

Цикл оптимизирован вне , как мы видим на godbolt .

call std::chrono::_V2::steady_clock::now()
mov r12, rax
call std::chrono::_V2::steady_clock::now()

Код был скомпилирован с:

g++  bench.cpp  -Wall  -Wextra -std=c++11 -O3

и gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1) на процессоре Intel® Xeon® W-2195 .

Я был единственным пользователем на машине, и я стараюсь работать с и без наивысшего приоритета (nice или chrt), и результат был одинаковым.

РезультатЯ получил с 100 000 000 итераций было:

100 000 000 iterations

Ось Y в наносекундах , это результат линии

std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()

Эти 4 строки заставляют меня задуматься: нет пропусков кэша / L1 / L2 / L3 (даже если строка «L3 cache misses» кажется слишком близкой к линии L2)

Я не уверен, почему будут пропуски в кеше, возможно, это результат хранения, но его нет в измеряемом коде.

Я попытался запустить программу 10 000 раз сцикл 1500, потому что кэш L1 этого процессора:

lscpu | grep L1 
L1d cache:             32K
L1i cache:             32K

и 1500*16 bits = 24 000 bits, что меньше 32 КБ, поэтому не должно быть пропуска кеша.

И результаты:

10 000 time the program with a loop of 1500

У меня все еще есть 4 строки (и немного шума).

Так что, если это действительно промах кеша, я неЯ имею представление, почему это происходит.

Я не знаю, полезно ли это для вас, но я запускаю:

sudo perf stat -e cache-misses,L1-dcache-load-misses,L1-dcache-load  ./a.out 1000

Со значением 1 000 / 10 000 / 100 000 / 1 000 000

Iполучил от 4,70 до 4,30% всех обращений к L1-dcache, что мне кажется довольно приличным.

Итак, вопросы:

  • В чем причинаиз этих замедлений?
  • Как произвести качественный тест функции, когда у меня нет постоянного времени для операции «Нет»?

PS: я неКвон, если яотсутствует полезная информация / флаги, не стесняйтесь спрашивать!


Как воспроизвести :

  1. Код:

    #include <iostream>
    #include <chrono>
    #include <vector>
    
    int main(int argc, char **argv)
    {
        int statSize = 1000;
        using clock = std::chrono::steady_clock;
        if (argc == 2)
        {
            statSize = std::atoi(argv[1]);
        }
    
        std::vector<uint16_t> temps;
        temps.reserve(statSize);
        for (int i = 0; i < statSize; i++)
        {
    
            auto t1 = clock::now();
    
            auto t2 = clock::now();
    
            temps.push_back(
                std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count());
        }
    
        for (auto t : temps)
            std::cout << (int)t << std::endl;
    
        return (0);
    }
    
  2. Сборка:

    g++  bench.cpp  -Wall  -Wextra -std=c++11 -O3
    
  3. Генерировать вывод (необходимо sudo):

    В этом случае я запускаю программу 10 000 раз.Каждый раз беру 100 тактов, и я удаляю первое, которое всегда примерно в 5 раз медленнее:

     for i in {1..10000} ; do sudo nice -n -17 ./a.out 100 | tail -n 99  >> fast_1_000_000_uint16_100 ; done
    
  4. Создание графика:

    cat fast_1_000_000_uint16_100 | gnuplot -p -e "plot '<cat'"
    
  5. Результат, который я имею на моей машине:

enter image description here


Где я нахожусь после ответа Зулан и все комментарии

current_clocksource установлен на tsc, а переключатель dmesg не виден, используемая команда:

dmesg -T | grep tsc

I useэтот сценарий для удаления HyperThreading (HT), затем

grep -c proc /proc/cpuinfo
=> 18

Вычтите 1 из последнего результата, чтобы получить последнее доступное ядро:

=> 17

Редактировать / etc / grub /по умолчанию и добавить isolcpus = (последний результат) в GRUB_CMDLINE_LINUX:

GRUB_CMDLINE_LINUX="isolcpus=17"

Окончательно:

sudo update-grub
reboot 
// reexecute the script

Теперь я могу использовать:

taskset -c 17 ./a.out XXXX

Итак, я запускаю10 000 раз цикл из 100 итераций.

for i in {1..10000} ; do sudo /usr/bin/time -v taskset -c 17 ./a.out 100  > ./core17/run_$i 2>&1 ; done

Проверьте, есть ли Involuntary context switches:

grep -L "Involuntary context switches: 0" result/* | wc -l
=> 0 

Нет, хорошо.Давайте построим график:

for i in {1..10000} ; do cat ./core17/run_$i | head -n 99 >> ./no_switch_taskset ; done
cat no_switch_taskset | gnuplot -p -e "plot '<cat'"

Результат:

22 fail (sore 1000 and more) in 1 000 000

Есть еще 22 меры, превышающие 1000 (при большинстве значений около 20) что я не понимаю.

Следующий шаг, TBD

Выполните часть:

sudo nice -n -17 perf record...

из ответа Зулана

1 Ответ

3 голосов
/ 18 июня 2019

Я не могу воспроизвести его с этими конкретными кластерными строками, но вот некоторая общая информация.

Возможные причины

Как обсуждалось в комментариях, приятно на обычном холостом ходуСистема - это всего лишь лучшее усилие .У вас все еще есть по крайней мере

  1. Таймер расписания для планирования

  2. Задачи ядра, связанные с определенным кодом

  3. Ваша задача может быть перенесена из одного ядра в другое по произвольной причине

Вы можете использовать isolcpus и taskset для получения эксклюзивных ядер для некоторых процессов, чтобы избежать некоторых из этого, но я не думаю, что вы действительно можете избавиться от всех задач ядра.Кроме того, используйте nohz=full, чтобы отключить тик планирования .Вы также должны отключить гиперпоточность, чтобы получить эксклюзивный доступ к ядру из аппаратного потока.

За исключением taskset, который я абсолютно рекомендую для любого измерения производительности, это довольно необычные меры.

Измерьте вместо того, чтобы угадывать

Если есть подозрение, что может произойти, вы обычно можете настроить измерение, чтобы подтвердить или опровергнуть эту гипотезу.perf и трассировки отлично подходят для этого.Например, мы можем начать с рассмотрения действий по планированию и некоторых прерываний:

sudo nice -n -17 perf record -o perf.data -e sched:sched_switch -e irq:irq_handler_entry -e irq:softirq_entry ./a.out ...

perf script теперь сообщит вам список всех случаев.Чтобы сопоставить это с медленными итерациями , вы можете использовать perf probe и слегка измененный тест:

void __attribute__((optimize("O0"))) record_slow(int64_t count)
{
    (void)count;
}

...

    auto count = std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
    if (count > 100) {
        record_slow(count);
    }
    temps.push_back(count);

и скомпилировать с -g

sudo perf probe -x ./a.out record_slow count

Затем добавьте-e probe_a:record_slow на звонок perf record.Теперь, если вам повезет, вы обнаружите несколько близких событий, например:

 a.out 14888 [005] 51213.829062:    irq:softirq_entry: vec=1 [action=TIMER]
 a.out 14888 [005] 51213.829068:  probe_a:record_slow: (559354aec479) count=9029

Имейте в виду: хотя эта информация, вероятно, объяснит некоторые ваши наблюдения, вы попадаете в мир еще более загадочных вопросов и странностей.Кроме того, в то время как perf довольно низко накладные расходы, могут быть некоторые возмущения в том, что вы измеряете.

Что мы измеряем?

Прежде всего, вам нужно четко понимать, что выФактическая мера: время выполнения std::chrono::steady_clock::now().На самом деле хорошо сделать это, чтобы выяснить как минимум эти накладные расходы, а также точность часов.

Это на самом деле сложный вопрос.Стоимость этой функции с clock_gettime снизу зависит от вашего текущего источника синхронизации 1 .Если это tsc, то все в порядке - hpet намного медленнее .Linux может тихо переключаться 2 с tsc на hpet во время работы.

Что делать, чтобы получить стабильные результаты?

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

Если ядро ​​измерения не значительно длиннее точности таймера, вам придется повторить ядро ​​и измерить снаружи, чтобы получить пропускную способностьа не задержка, которая может или не может быть разной.

Да - правильное тестирование очень сложно , вам нужно учитывать множество аспектов, особенно когда вы приближаетесь к аппаратному обеспечениюи ваши времена ядра становятся очень короткими.К счастью, есть некоторая помощь, например, Библиотека эталонных тестов Google предоставляет большую помощь с точки зрения выполнения правильного количества повторений, а также с точки зрения наличия факторов эксперимента.

1 /sys/devices/system/clocksource/clocksource0/current_clocksource

2 На самом деле он находится в dmesg как что-то вроде

Clocksource: сторожевой таймер на CPU: Маркировка Clocksource 'tsc' как нестабильной, потому чтослишком большой перекос:

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