Еще раз о Hyper-Threading

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

    Исследуемая платформа


    Объект экспериментов – ноутбук ASUS N750JK c процессором Intel Core i7-4700HQ. Тактовая частота 2.4GHz, повышаемая в режиме Intel Turbo Boost до 3.4GHz. Установлено 16 гигабайт оперативной памяти DDR3-1600 (PC3-12800), работающей в двухканальном режиме. Операционная система – Microsoft Windows 8.1 64 бита.

    image
    Рис.1 Конфигурация исследуемой платформы.

    Процессор исследуемой платформы содержит 4 ядра, что при включении технологии Hyper-Threading обеспечивает аппаратную поддержку 8 потоков или логических процессоров. Эту информацию Firmware платформы передает операционной системе посредством ACPI-таблицы MADT (Multiple APIC Description Table). Поскольку платформа содержит только один контроллер оперативной памяти, таблица SRAT (System Resource Affinity Table), декларирующая приближенность процессорных ядер к контроллерам памяти, отсутствует. Очевидно, исследуемый ноутбук не является NUMA-платформой, но операционная система, в целях унификации, рассматривает его как NUMA-систему с одним доменом, о чем говорит строка NUMA Nodes = 1. Факт, принципиальный для наших экспериментов – кэш память данных первого уровня имеет размер 32 килобайта на каждое из четырех ядер. Два логических процессора, разделяющие одно ядро, используют кэш-память первого и второго уровней совместно.

    Исследуемая операция


    Исследовать будем зависимость скорости чтения блока данных от его размера. Для этого выберем наиболее производительный метод, а именно чтение 256-битных операндов посредством AVX-инструкции VMOVAPD. На графиках по оси X отложен размер блока, по оси Y – скорость чтения. В окрестности точки X, соответствующей размеру кэш-памяти первого уровня, ожидаем увидеть точку перегиба, поскольку производительность должна упасть после того, как обрабатываемый блок выйдет за пределы кэш-памяти. В нашем тесте, в случае многопоточной обработки, каждый из 16 инициируемых потоков, работает с отдельным диапазоном адресов. Для управления технологией Hyper-Threading в рамках приложения, в каждом из потоков используется API-функция SetThreadAffinityMask, задающая маску, в которой каждому логическому процессору соответствует один бит. Единичное значение бита разрешает использовать заданный процессор заданным потоком, нулевое значение – запрещает. Для 8 логических процессоров исследуемой платформы, маска 11111111b разрешает использовать все процессоры (Hyper-Threading включен), маска 01010101b разрешает использовать по одному логическому процессору в каждом ядре (Hyper-Threading выключен).

    На графиках используются следующие сокращения:

    MBPS (Megabytes per Second)скорость чтения блока в мегабайтах в секунду;

    CPI (Clocks per Instruction)количество тактов на инструкцию;

    TSC (Time Stamp Counter)счетчик процессорных тактов.

    Примечание.Тактовая частота регистра TSC может не соответствовать тактовой частоте процессора при работе в режиме Turbo Boost. Это необходимо учитывать при интерпретации результатов.

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

    Опыт №1. Один поток


    image
    Рис.2 Чтение одним потоком

    Максимальная скорость 213563 мегабайт в секунду. Точка перегиба имеет место при размере блока около 32 килобайт.

    Опыт №2. 16 потоков на 4 процессора, Hyper-Threading выключен


    image
    Рис.3 Чтение шестнадцатью потоками. Количество используемых логических процессоров равно четырем

    Hyper-Threading выключен. Максимальная скорость 797598 мегабайт в секунду. Точка перегиба имеет место при размере блока около 32 килобайт. Как и ожидалось, по сравнению с чтением одним потоком, скорость выросла приблизительно в 4 раза, по количеству работающих ядер.

    Опыт №3. 16 потоков на 8 процессоров, Hyper-Threading включен


    image
    Рис.4 Чтение шестнадцатью потоками. Количество используемых логических процессоров равно восьми

    Hyper-Threading включен. Максимальная скорость 800722 мегабайт в секунду, в результате включения Hyper-Threading почти не выросла. Большой минус – точка перегиба имеет место при размере блока около 16 килобайт. Включение Hyper-Threading немного увеличило максимальную скорость, но падение скорости теперь наступает при вдвое меньшем размере блока – около 16 килобайт, поэтому существенно упала средняя скорость. Это не удивительно, каждое ядро имеет собственную кэш-память первого уровня, в то время, как логические процессоры одного ядра, используют ее совместно.

    Выводы


    Исследованная операция достаточно хорошо масштабируется на многоядерном процессоре. Причины – каждое из ядер содержит собственную кэш-память первого и второго уровней, размер целевого блока сопоставим с размером кэш-памяти, и каждый из потоков работает со своим диапазоном адресов. В академических целях мы создали такие условия в синтетическом тесте, понимая, что реальные приложения обычно далеки от идеальной оптимизации. А вот включение Hyper-Threading, даже в этих условиях дало негативный эффект, при небольшой прибавке пиковой скорости, имеет место существенный проигрыш в скорости обработки блоков, размер которых находится в диапазоне от 16 до 32 килобайт.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 18

      +2
      А что у вас за задача такая, что вам нужно постоянно читать один и тот же блок данных размером строго больше 16 килобайт, но строго меньше 32, что вы смогли почувствовать замедление?
        0
        Как показано на графиках, мы читаем блоки размером от 128 байт до 96000 байт:
        На графиках по оси X отложен размер блока

        Почему возник вопрос именно о блоках строго больше 16 килобайт, но строго меньше 32?
          +3
          Про графики мне все понятно. Мне не понятно практическое применение, где может просесть скорость. Где в практической задаче может понадобиться постоянно читать один и тот же блок данных размером от 16 до 32.

          Мне-то кажется, что тест у вас сугубо синтетический и в реальности отражения не имеет, но вдруг я ошибаюсь.

          Кстати, непонятно, почему вы мочите именно Hyper-Threading, можно же и без него создать еще столько же потоков на тех же ядрах. И сделать вывод несколько шире и умнее, чем просто «Hyper-Threading» зло.

          Например, можно сделать вывод, что если суммарный размер одновременно обрабатываемых данных на всех ядрах не вмещается в суммарный кеш L1 на этих же ядрах, то производительность резко падает. И получить за это медальку от Капитана Очевидности.

          Так при чем тут Hyper-Threading?
            +1
            • Мы не делаем глобальных выводов о вреде Hyper-Threading, а рассматриваем его эффект исключительно в описываемом контексте. Есть много задач где HT приносит пользу. В процессе исследования топологии CoD выяснилось, что HT уже не дает того положительного эффекта, который был на заре его восхождения. Стало интересно, насколько востребованным он остается на современных системах, которые «не дотягивают» до NUMA-платформ.
            • Повторные чтения могут быть например, при перемножении матриц A и B, когда строка матрицы A постоянно находится в кэш-памяти, а столбцы матрицы B загружаются последовательно. Кстати, при отсутствии повторных обращений к данным, вообще не было бы смысла в кэш-памяти.
            • Поскольку L1 свой для каждого из ядер, но общий для двух логических процессоров одного ядра, эффект от распараллеливания по ядрам (сравнение опытов 1 и 2) позитивный, а от распараллеливания по логическим процессорам (сравнение опытов 2 и 3) негативный, с точки зрения средней скорости, но не пиковой. Мы как раз сделали акцент на это различие.
              +1
              По поводу второго пункта: имело бы, см. префетч.
                0
                Если гипотетически представить ситуацию, при которой кэш-память используется исключительно как буфер, в котором хранятся данные от момента их опережающего чтения (аппаратного Prefetch или программных Prefetch Hints) до использования, но чтение этих данных происходит однократно, то некая прибавка производительности конечно будет, хотя небольшая, по сравнению с эффектами от повторных чтений. В этом случае правильнее было бы говорить о буферизации, а мы говорим о кэшировании.

                Отметим, что буферы чтения и записи есть в процессоре, отдельно от кэш-памяти.
        +4
        HT позволяет не простаивать процессору ожидая данных из памяти для первого потока благодаря переключению на второй поток. Но если и второй запрашивает данные из памяти которых в кеше нет то оба потока будут ждать. Скорость работы с памятью это не сильно повысит а вот производительность ядра повышает.

          0
          Мы предполагаем, что позитивный эффект от Hyper-Threading будет в ситуации, когда два логических процессора одного ядра заняты разными задачами, например вычисления с плавающей запятой и копирование блоков памяти, в этом случае они используют разные ресурсы, а не «стоят в очереди» при доступе к одному ресурсу. Так это или нет — покажут дальнейшие опыты
            +1
            Тогда позвольте озвучить некоторые «хотелки». Для многих, я думаю, они действительно будут полезны (для меня естественно тоже).

            Первое — было бы просто замечательно, если бы Вы проделали эти тесты плавно изменяя кол-во потоков от 1-го до 4*N, где N = кол-во физических ядер. Сначала без HT, затем с ним. Зачем с потоками < N (количества ядер) замерять производительность с включенным и выключенным HT? Для полноты картины и проверки теории практикой.

            Второе — было бы полезным плавно менять пересечение инструкций. Я имею ввиду, что полное отсутствие борьбы за исполнительные блоки, должно привести, по словам Intel, к выигрышу в скорости до 30%. Постепенное введение в смежные потоки одинаковых инструкций, должно замедлять нас. Пример на пальцах: в смежных потоках выполняется 9 разных инструкций и одна общая — вероятность пересечения (борьбы за исполнительный блок) 1/10. Постепенное увеличение степени пересечений инструкций, приведет нас к последовательному исполнению. Вопрос только в том, подтвердится ли это тестами.

            Третье — мы уже знаем из этой статьи про промахи кэша. Значит необходимо все тесты произвести еще в двух комбинациях.

            Повторюсь, это всего лишь «хотелки», которые, как мне кажется, более глубоко раскроют суть вопроса. В любом случае, жду с нетерпением.
              0
              По поводу использования различных инструкций в двух потоках, выполняемых одним ядром — мы планируем такой эксперимент, в частности один поток выполняет чтение AVX-инструкциями, а второй — строковыми инструкциями. Но предполагаю, что польза от HT будет больше при другом сценарии — один поток читает
              кэш-память, а второй выполняет вычисления с плавающей точкой. Но это уже другой тип задач.

              По поводу плавного увеличения количества ядер. Думаю, что поведение будет достаточно предсказуемым, точка перегиба на графике будет при обработке 32K на ядро, независимо от того, как эти 32K распределены по потокам. Нас и за проверку менее очевидных фактов подвергли критике. Хотя, в любом случае, в будущих версиях нашего теста планируется ввести возможность выбора количества потоков с дискретностью 1.
          +1
          Статья больше про cache miss, чем про HT.
          Суть HT в том, что загруженные в кэш данные (разных потоков) могут обрабатываться одновременно, если для этого им нужны разные исполнительные блоки. Или, если один поток ждет чего-либо, то второй исполняется. В тесте данные загружаются. Ни какой обработки. Так при чем тут HT? У Вас и получился ожидаемый результат — два потока кода делят один кэш.
          Поправьте меня, если я не прав.

          Вы же наверняка догадываетесь, как Ваш третий тест может иметь такой же результат как и во втором тесте? Сократим количество областей памяти с 16 до 8 — по два потока на участок.
          Так может все же статья про промахи кэша?

          Плюс, почему то нет теста с четырьмя потоками.
            0
            см. ниже
            0
            Абсолютно верно — падение производительности вызывается промахами кэша. Принципиально то, что когда мы делим потоки по логическим процессорам одного ядра, промахи начинаются в два раза раньше (при блоках 16K), чем тогда, когда делим потоки по ядрам. Причина — у каждого ядра свой кэш L1, а для двух логических процессоров одного ядра он общий. Вот на этой особенности Hyper-Threading мы и акцентировали внимание.
              0
              Принципиально то, что когда мы делим потоки по логическим процессорам одного ядра, промахи начинаются в два раза раньше
              На раньше. Если у вас два потока пользуются одним кэшем, то суммарно падение начинается при том же объеме обрабатываемых данных.

              Вот на этой особенности Hyper-Threading мы и акцентировали внимание.
              Я все равно не понимаю, почему это именно Hyper-Threading. Узким местом в задаче является только кэш. Хоть вы используете HT, хоть не используете, как только на одном кэше у вас обрабатывается данных больше, чем в него влазит, скорость падает. И падает она одинаково, используете вы один поток на одном логическом процессоре, два потока на разных логических процессорах, или же десять потоков на одном логическом процессоре. Без разницы.
                0
                При любом сценарии скорость падает, когда суммарный размер блоков, обрабатываемых одним физическим ядром, превышает размер кэш-памяти (в нашем случае 32К). Это может быть 32K одного потока (без HT) или сумма размеров блоков, обрабатываемых двумя логическими процессорами одного ядра. Акцентируем, что при оптимизации, размеры блоков следует выбирать в зависимости от типа распараллеливания Hyper-Threading или Multi-Core.

                Факт, пусть и очевидный, но требующий численной оценки, которую мы и попытались сделать.
                  0
                  Это может быть 32K одного потока (без HT) или сумма размеров блоков, обрабатываемых двумя логическими процессорами одного ядра.
                  Или же нескольких потоков на одном ядре без всякого гипертрейдинга, что вы продолжаете игнорировать. Еще раз: дело не в гипертрейдинге, а в количестве данных, обрабатываемых на одном экземпляре кэша (в данном случае на одном ядре). Способ достижения порога в 32Кб роли не играет.
                    +1
                    Способ достижения порога в 32К роли не играет. Но для программиста, при назначении заданий потокам, нужно оптимизировать размеры блоков, обрабатываемых потоками. И для этой оптимизации, важно знать мультипроцессорную топологию, в частности, используется ли Hyper-Threading?

                    Одного знания о количестве логических процессоров недостаточно. В нашем примере одна и та же маска Affinity Mask = 11111111b может соответствовать двум ситуациям:
                    1) 4 ядра 8 потоков, используется HT.
                    2) 8 ядер 8 потоков, не используется HT.
                    Чтобы выбрать оптимальный размер блока (в простейшем примере 16K или 32K), надо дифференцировать между ситуациями 1 и 2. У нас это вариант 1. Но та же маска может быть при варианте 2.

                    Вот на этой тонкости мы и акцентировали внимание.
                  0
                  По оси X откладывается размер блока, обрабатываемого каждым потоком.

              Only users with full accounts can post comments. Log in, please.