Дельта производительности, вызванная назначением или приращением указателя (строгий псевдоним?) - PullRequest
0 голосов
/ 01 февраля 2019

Обновление: Минимальный пример, демонстрирующий проблему в Clang 7.0 - https://wandbox.org/permlink/G5NFe8ooSKg29ZuS
https://godbolt.org/z/PEWiRk

Я испытываю изменение производительности функции отОт 0 до 500-900 мкс метода, основанного на 256 итерациях (Visual Studio 2017):

void* SomeMethod()
{
    void *result = _ptr; // _ptr is from malloc

    // Increment original pointer
    _ptr = static_cast<uint8_t*>(_ptr) + 32776;    // (1)

    // Set the back pointer
    *static_cast<ThisClass**>(result) = this;      // (2)

    return result;
}

Если я прокомментирую строки (1) или (2), время выполнения метода будет 0 мкс.Включение обеих строк приводит к временному интервалу от 2 мкс до 4 мкс на вызов функции.

Я не уверен, что нарушаю строгие правила наложения имен, и при наблюдении с помощью CompilerExplorer , яМожно видеть, что установка обратного указателя (строка (2)) генерирует только одну инструкцию:

mov QWORD PTR [rax], rcx

Что заставляет меня задуматься, может ли это быть строгим псевдонимом, приводящим к тому, что компилятор неоптимизировать, когда кажется, что единственным аффектом является 1 дополнительная инструкция для 1 строки кода.

Для справки, при увеличении исходного указателя (строка (1)) создаются две инструкции:

lea     rdx, QWORD PTR [rax+32776]
mov     QWORD PTR [rcx], rdx

И для полноты, вот полный вывод сборки:

mov     rax, QWORD PTR [rcx]
lea     rdx, QWORD PTR [rax+32776]
mov     QWORD PTR [rcx], rdx
mov     QWORD PTR [rax], rcx
ret     0

Что может быть причиной разницы в производительности?Сейчас я предполагаю, что код плохо работает с кешем процессора, но я просто не могу понять, почему включение одной команды перемещения может вызвать это?

1 Ответ

0 голосов
/ 04 февраля 2019

Если вы прокомментируете одну из этих строк, вы либо сохраняете один и тот же адрес несколько раз (и, вероятно, оптимизируете его в цикле), либо не сохраняете вообще.Неудивительно, что время неизмеримо мало и округляется до 0 микросекунд.

В тестовом коде, который вы связали, вы набираете 32 КБ на магазин, на недавно выделенной памяти без прогрева,Вы, вероятно, получаете мягкую ошибку страницы и копирование при записи при каждой итерации.(Память malloc ed, вероятно, все лениво отображалась на одну и ту же физическую нулевую страницу.)

256 итераций также совершенно недостаточно для того, чтобы разогнать процессор до нормальной / турбо тактовой частоты, вне скорости холостого хода.

На моем i7-6700k Arch Linux destkop (в режиме ожидания 800 МГц, нормальная тактовая частота 3,9 ГГц, регулятор / energy_performance_preference = balance_performance (не по умолчанию balance_power, поэтому он увеличивается быстрее):

Я скомпилировал с помощью gcc8.2.1 и запустил получившийся исполняемый файл в цикле с while ./a.out ;do :;done, чтобы процессор оставался на высокой тактовой частоте. Программа печатает такие моменты времени, как 1.125us + - немного. Это звучит примерно как для страницы-fault + обнуление страницы + обновление таблиц страниц и сброс TLB.


Используя Linux perf stat, я запускал его 100 раз с усреднением подсчетов. (Столбец вторичной статистики "rate" имеетфиктивные устройства из-за ошибки перфокарта, которую Arch еще не обновил, чтобы исправить. Таким образом, на самом деле он измеряет 4,4 ГГц (что я считаю фиктивным, Turbo отключен на моем процессоре, чтобы вентиляторы не работали).

peter@volta:/tmp$ perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,dtlb_store_misses.miss_causes_a_walk,tlb_flush.dtlb_thread,dtlb_load_misses.miss_causes_a_walk -r100 ./a.out


 Performance counter stats for './a.out' (100 runs):

              1.15 msec task-clock                #    0.889 CPUs utilized            ( +-  0.33% )
                 0      context-switches          #   40.000 M/sec                    ( +- 49.24% )
                 0      cpu-migrations            #    0.000 K/sec                  
               191      page-faults               # 191250.000 M/sec                  ( +-  0.09% )
         4,343,915      cycles                    # 4343915.040 GHz                   ( +-  0.33% )  (82.06%)
           819,685      branches                  # 819685480.000 M/sec               ( +-  0.05% )
         4,581,597      instructions              #    1.05  insn per cycle           ( +-  0.05% )
         6,366,610      uops_issued.any           # 6366610010.000 M/sec              ( +-  0.05% )
         6,287,015      uops_executed.thread      # 6287015440.000 M/sec              ( +-  0.05% )
             1,271      dtlb_store_misses.miss_causes_a_walk # 1270910.000 M/sec                 ( +-  0.21% )
     <not counted>      tlb_flush.dtlb_thread                                         (0.00%)
     <not counted>      dtlb_load_misses.miss_causes_a_walk                                     (0.00%)

        0.00129289 +- 0.00000489 seconds time elapsed  ( +-  0.38% )

Эти подсчеты включают режим ядра, но - это 191 сбоев страниц для 256 итераций цикла, так что огромное большинство времени, проведенного этой программой, находится в ядре.

И как только мы возвращаемся в пространство пользователя, более 1000 хранилищ вызывают пропуски dTLB, которые также пропускаются в TLB 2-го уровня, требуя обхода страницы.Но никаких загрузок не было.

Вероятно, мы могли бы получить намного более чистые данные, выделив намного больше памяти, чтобы мы могли увеличить Count без сегментации.Профилирование с perf record показывает, что только около 20% общего времени программы тратится на main;остальная часть находится в динамическом компоновщике / загрузке, частично от печати. ​​

...