Пользователь
0,0
рейтинг
17 апреля 2014 в 23:17

Разработка → Доступен новый JIT: теперь с поддержкой SIMD перевод

От переводчика


Лично я просто невероятно обрадовался новой возможности. Как раз не так давно одолел Pro .Net Perfomance, в которой одна из глав была посвящена параллельности, и векторизации в частности. Вывод, сделанный авторами: «К сожалению, использование векторизации возможно исключительно на С++, выполнение кода на видеокарте — возможно и средствами .Net, однако C++ AMP оставляет любые управляемые библиотеки GPGPU далеко позади, поэтому, к сожалению, в данных задачах рекомендуем использовать подключаемые C++ сборки.» Поэтому рад сообщить, что по крайней мере одна проблема решена. Что ж, приступим!



Вступление



Быстродействие процессоров более не подчиняется закону Мура. Поэтому для увеличения производительности приложений, все важнее использовать параллелизацию. Или, как говорил Герб Саттер, «бесплатного супа больше не будет» (The free lunch is over, отсылка к статье The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software, перевод на хабре)

Можно подумать, что использование задачеориентированного программирования (например, в случае .Net — TPL, прим. пер.) или обычных потоков уже решает эту проблему. В то время как многопоточность, безусловно, является важной задачей, нужно понимать, что по-прежнему важно оптимизировать код, выполняющийся на каждом отдельном ядре. SIMD это технология, которая использует распараллеливание данных на уровне процессора. Многопоточность и SIMD дополняют друг друга: многопоточность позволяет распараллеливать работу на несколько ядер процессора, в то время как SIMD позволяет распараллеливать работу в пределах одного ядра.

Сегодня мы рады объявить о новой превью-версии RyuJIT, которая обеспечивает функциональность SIMD. SIMD API доступен через новый пакет NuGet, Microsoft.Bcl.Simd, который также выпущен в виде превью.

Вот пример, как вы можете его использовать:

// Initalize some vectors

Vector<float> values = GetValues();
Vector<float> increment = GetIncrement();

// The next line will leverage SIMD to perform the
// addition of multiple elements in parallel:

Vector<float> result = values + increment;


Что такое SIMD и зачем он мне?



SIMD на данный момент является самой востребованной фичей, которую просят добавить в платформу (около 2000 голосов пользователей)

image

Он настолько популярен, потому что, за исключением некоторых видов приложений, SIMD существенно ускоряет работу кода. Например, производительность рендеринга множества Мандельброта можно ощутимо улучшить с помощью SIMD: в 2-3 раза (если процессор поддерживает SSE2), и в 4-5 раз (если процессор поддерживает AVX).

image

image

Введение в SIMD



SIMD расшифровывается как «одна команда, множество данных» (отечественный вариант — ОКМД, прим. пер). Это набор инструкций процессора, которые позволяют работать над векторами вместо скаляров. За счет этого возможно параллельное выполнение математических операций над множеством данных.

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

float[] values = GetValues();
float increment = GetIncrement();

// Perform increment operation as manual loop:
for (int i = 0; i < values.Length; i++)
{
    values[i] += increment;
}


SIMD же позволяет прибавлять несколько значений одновременно, используя специфические инструкции CPU. Обычно это выглядит, как операция над вектором:

Vector<float> values = GetValues();
Vector<float> increment = GetIncrement();

// Perform addition as a vector operation:
Vector<float> result = values + increment;


Интересно отметить, что существует не одна спецификация SIMD. Скорее, каждый процессор имеет свою собственную реализацию SIMD. Они отличаются числом элементов, над которыми операция может выполняться одновременно, а также набором доступных инструкций. Наиболее распространенной реализацией SIMD на процессорах Intel / AMD является SSE2.

Вот упрощенная модель того, как работает SIMD на уровне процессора:

  1. В процессоре есть специальные SIMD регистры. Они имеют фиксированный размер. Например, для SSE2, размер 128 бит.
  2. Процессор также имеет специальные SIMD-инструкции, зависящие от размера операнда. С точки зрения процессора, данные в SIMD регистре являются просто набором бит. Однако, разработчик хочет интерпретировать эти биты как, ну скажем, набор 32-битных целых чисел. Для этой цели в процессоре есть инструкции, специфичные для выполняемой операции (например, сложения), и типа операнда (например, 32-битного целого)


Одна из многих областей, где SIMD является весьма полезным — это графика и игры, так как:
  • Эти приложения выполняют очень много вычислений.
  • Большинство структур данных уже представлены как векторы.

Однако SIMD применим к любому типу приложения, которое выполняет числовые операции на большом наборе значений. Сюда же можно отнести научные расчеты и финансы

Проектирование SIMD для. NET



Большинство .NET разработчиков не должно писать CPU-зависимый код. Вместо этого CLR абстрагирует аппаратное обеспечение, предоставляя виртуальную машину, которая переводит свой ​​код в машинные команды либо во время выполнения (JIT), либо во время установки (NGEN). Оставляя генерацию кода CLR, вы можете использовать один и тот же MSIL код на разных компьютерах с разными процессорами, не отказываясь от оптимизаций, специфических для данного конкретного CPU.

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

Для того чтобы сохранить это разделение, мы должны были придумать такую модель программирования для SIMD, которая бы позволила выразить векторные операции без привязки к конкретной реализации процессора, например SSE2. Мы придумали модель, которая обеспечивает две категории векторных типов:

image

Эти две категории мы называем JIT-«встроенными типами». Это означает, что JIT знает об этих типах и трактует их особым образом при генерации машинного кода. Тем не менее, все типы также разработаны для безупречной работы в тех случаях, когда оборудование не поддерживает SIMD (что встречается сегодня довольно редко) или приложение не использует эту версию RyuJIT. Наша цель заключается в обеспечении приемлимой производительности даже в тех случаях, когда она примерно такая же, как и для последовательного написанный кода. К сожалению, в этом превью мы пока этого не достигли.

Векторы с фиксированным размером



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

Ключевым аспектом является то, что эти векторы имеют определенное количество элементов, как правило, два, три или четыре. Двухэлементные векторы часто используется для представления точки или подобных объектов, таких как комплексные числа. Векторы с тремя и четырьмя элементами, как правило, используется для 3D (4-й элемент используется, чтобы сделать математическую работу [Очевидно, речь идет о том, что в 3D-моделировании преобразования используют четырехмерные матрицы XYZ1, прим. пер]). Суть в том, что эти задачи используют векторы с определенным количеством элементов.

Чтобы получить представление о том, как эти типы выглядят, посмотрим на упрощенную версию Vector3f:
public struct Vector3f
{
    public Vector3f(float value);
    public Vector3f(float x, float y, float z);
    public float X { get; }
    public float Y { get; }
    public float Z { get; }
    public static bool operator ==(Vector3f left, Vector3f right);
    public static bool operator !=(Vector3f left, Vector3f right);
    // With SIMD, these element wise operations are done in parallel:
    public static Vector3f operator +(Vector3f left, Vector3f right);
    public static Vector3f operator -(Vector3f left, Vector3f right);
    public static Vector3f operator -(Vector3f value);
    public static Vector3f operator *(Vector3f left, Vector3f right);
    public static Vector3f operator *(Vector3f left, float right);
    public static Vector3f operator *(float left, Vector3f right);
    public static Vector3f operator /(Vector3f left, Vector3f right);
}


Я хотел бы подчеркнуть следующие аспекты:

  • Мы разработали векторы фиксированного размера, так что они могут легко заменить те, которые определены в приложениях.
  • По соображениям производительности, мы определили эти типы, как неизменяемые типы значений.
  • Идея состоит в том, что после замены вашего вектора нашим, ваше приложение будет вести себя так же, за исключением того, что оно будет работает быстрее. Для получения более подробной информации, посмотрите на наш пример приложения трассировки лучей.


Векторы с аппаратно-зависимым размером



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

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

Чтобы понять для разницу, ниже представлена упрощенная версия
Vector:
public struct Vector<T> where T : struct { public Vector(T value); public Vector(T[] values); public Vector(T[] values, int index); public static int Length { get; } public T this[int index] { get; } public static bool operator ==(Vector<T> left, Vector<T> right); public static bool operator !=(Vector<T> left, Vector<T> right); // With SIMD, these element wise operations are done in parallel: public static Vector<T> operator +(Vector<T> left, Vector<T> right); public static Vector<T> operator &(Vector<T> left, Vector<T> right); public static Vector<T> operator |(Vector<T> left, Vector<T> right); public static Vector<T> operator /(Vector<T> left, Vector<T> right); public static Vector<T> operator ^(Vector<T> left, Vector<T> right); public static Vector<T> operator *(Vector<T> left, Vector<T> right); public static Vector<T> operator *(Vector<T> left, T right); public static Vector<T> operator *(T left, Vector<T> right); public static Vector<T> operator ~(Vector<T> value); public static Vector<T> operator -(Vector<T> left, Vector<T> right); public static Vector<T> operator -(Vector<T> value); }


Ключевые аспекты этого типа включают в себя следующее:

  • Он обобщенный. Для повышения гибкости и во избежание комбинаторного взрыва типов, мы определили аппаратно-зависимый вектор как обобщенный тип, Vector. Исходя из практических соображений, Т может быть только примитивным числовым типом. В этом превью мы поддерживаем только int, long, float и double. Окончательный вариант будет также включать в себя поддержку всех других целочисленных типов, в том числе и их беззнаковых аналогов.

    Длина статична. Так как длина зависит от оборудования, но фиксирована, то доступ к ней предоставляется через статическое свойство Length. Размер же определяется как sizeof(SIMD-register) / sizeof(T). Другими словами, два вектора Vector и Vector имеют одинаковую длину, если T1 и T2 имеют одинаковый размер. Это позволяет нам соотнести элементы в векторах с элементами разных типов, что является очень полезным свойством для векторизованного кода.


    Векторизация - это сложная тема и, таким образом, она выходит за рамки этого блога. Тем не менее, позвольте мне дать вам общий обзор того, что это будет означать для конкретного приложения. Давайте посмотрим на средства визуализации множества Мандельброта. Концептуально, оно работает над комплексными числами, которые могут быть представлены в виде векторов с двумя элементами. Основываясь на математическом алгоритме, эти комплексные числа имеют цветовую маркировку и отображается в виде одной точки получаемого изображения.

    При наивном использовании SIMD, можно было бы векторизовать алгоритм, представляя комплексные числа как Vector2f. Более сложный алгоритм будет векторизовать точки для визуализации (число которых является неограниченным) вместо размера (который фиксирован). Один из способов сделать это заключается в представлении действительной и мнимой компонент, как векторов. Другими словами, можно было бы векторизовать один и тот же компонент над несколькими точками.

    Для получения более подробной информации, посмотрите на наш пример. В частности, сравните скалярную и векторизованную версии.

    Использование превью-версии SIMD



    В предпросмотре мы предоставляем следующие две части:

    1. Новый релиз RyuJIT, который обеспечивает поддержку SIMD
    2. Новая библиотека NuGet, который предоставляет поддержку SIMD


    Библиотека NuGet была специально предназначена для работы без поддержки SIMD со стороны аппаратных средств / JIT. В этом случае все методы и операции реализованы в виде чистого IL. Тем не менее,очевидно, что вы можете получить максимальную производительность только при использовании этой библиотеки в сочетании с новой версией RyuJIT.

    Для того чтобы использовать SIMD, вам необходимо выполнить следующие шаги:

    1. Скачать и установить последнюю превью RyuJIT с aka.ms/RyuJIT
    2. Установить некоторые переменные окружения для активизации новой JIT и SIMD для вашего процесса. Самый простой способ сделать это путем создания пакетного файла, который запускает ваше приложение:

      @echo off
      set COMPLUS_AltJit=*
      set COMPLUS_FeatureSIMD=1
      start myapp.exe
      

    3. Добавьте ссылку на NuGet-пакет Microsoft.Bcl.Simd. Вы можете сделать это, щелкнув правой кнопкой мыши на проекте и выбрав Manage NuGet References. В следующем диалоговом окне выберите вкладку под названием Online. Кроме того, необходимо выбрать Include Prelease в выпадающем списке на самом верху. Затем введите в текстовое поле в верхнем правом углу слово Microsoft.Bcl.Simd. Нажмите кнопку Install.


    В данном превью существуют определенные ограничения, о которых, возможно, вам стоит знать:

    • SIMD доступна только для 64-битных процессов. Поэтому убедитесь, что ваше приложение либо ориентировано именно на x64, либо скомпилировано как Any CPU, а не отмечено как 32-битное
    • Тип Vector поддерживает только int, long, float и double. Создание экземпляров Vector с любым другим типом вызовет исключение загрузки типа.

      SIMD на данный момент поддерживает только SSE2. В связи с некоторыми ограничениями реализации, RyuJIT CTP не может автоматически изменять размер типа на основании местных возможностей аппаратного обеспечения. Полная поддержка AVX должна появиться с выпуском рантайма .NET, который будет включать в себя RyuJIT. Мы начали работу над этим, но она (поддержка) пока что не сочетается с нашей моделью развертывания CTP.
    • .

      Мы хотели бы получить ваши отзывы об обоих проектах. Нравится ли вам такая модель программирования? Чего не хватает? Какие улучшения производительности нужны для вашего приложения? Пожалуйста, используйте комментарии для обеспечения обратной связи или отправьте нам письмо по адресу ryujit(at)microsoft.com.
Перевод: Immo Landwerth
Алексей @PsyHaSTe
карма
9,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (10)

  • +2
    Ну наконец-то. А то в Mono эта штука уже лет 6 как сделана была (см. Mono.Simd).
    • 0
      Ну так LLVM поддерживает SIMD инструкции из коробки и плюс к тому сама умеет векторизовать циклы, причем довольно сложные. Разработчики утверждают, что здесь они превосходят возможности GCC. Начиная с версии 3.2 LLVM научилась даже работать с условиями в циклах.

      Например, LLVM может векторизовать вот такой код не смотря на то, что в цикле есть зависимая переменная и условие:

      int foo(int *A, int *B, int n) {
          unsigned sum = 0;
      
          for (int i = 0; i < n; ++i)
              if (A[i] > B[i])
                  sum += A[i] + 5;
      
          return sum;
      }
      


      P.S.: Жду не дождусь, когда можно будет это применить в llst ;)
  • 0
    Не совсем то, но тоже о SIMD: его поддержка появляется в JavaScript. Что не может не радовать.
  • +3
    Требуется 64-битная платформа — это ладно. Но Windows 8.1 то зачем?
  • +4
    Нольшой коммент про графические приложения и 4-вектора, четвёртая координата имеет название и обычно обозначается w, это гомогенная координата, без такого представления сложно разделить понятие точки и вектора в трехмерных координатах и реализовать многие трансформации, например разные типы перспектив и перенос.
  • –1
    Это все здорово, но мне кажется тем кому нужны high-performance вычисления все могут и на С++ написать. Более того, популярные .NET математические библиотеки (например NMath), если на них посмотреть, являются всего лишь обертками вокруг C++ либ.

    Более того, у .NET пока нет встроенной поддержки GPU или Xeon Phi, не говоря уже о более экзотичных механизмах.
    • +1
      Весьма полезная штука для игроделов, где саму игру пишут на удобном и современном С#, а Pathfinding на С/С++.
      • +1
        Так никто не мешает игру писать на C#, но когда нужно посчитать что-то data-parallel, никто не мешает залочить массив и кинуть указатель в C++ную либу через P/Invoke. Я по крайней мере именно так делаю когда например нужно Bitmap какой-нть обработать. И в С++ уже все это «на ура» делается с SIMD, OpenMP и другими плюшками.
        • 0
          Смешивать языки в одном проекте вообще дело неблагодарное. К примеру .NET проект не может иметь «референс» на проект С++, а только на скомпилированную сборку, значит надо руками настраивать процесс билда(на билд сервере к примеру). Проект на С++ не может быть собран на машине без Windows SDK, а чисто .NET проект может быть собран на любой машине с .NET Framework нужной версии. Ну и наконец у проекта на .NET есть шанс запуститься под Mono, а с нативными библиотеками такого шанса нет(там даже xbuild не будет собирать проект С++).
          Ну а про выполнение такого «гибридного» кода я молчу. Access violation и прощай весь процесс.

          При этом всё что я выше описал взято из опыта.
          • –2
            Тут еще надо разделить Managed C++ и просто С++. Managed — это отдельная песня, я бы его не трогал т.к. непонятно зачем. Механизм P/Invoke нормально работает (при условии что не нужно ООП, а оно обычно не нужно для вычислений). Да, никаких «ссылок на проект», то есть в dependencies указать можно, но дальше, все что нужно — это чтобы нативная DLLка копировалась в ту же папку что и приложение и все, дальше P/Invoke ее сам найдет.

            Касательно переносимости кода, С++ намного лучше .NET. Не знаю как поднять Mono на Xeon Phi, да и на обычном Linux'е как-то пока не тянет.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.