Использует ли R SIMD при выполнении векторизованных вычислений?

Имея такой фрейм данных в R:

+---+---+
| X | Y |
+---+---+
| 1 | 2 |
| 2 | 4 |
| 4 | 5 |
+---+---+

Если в этом кадре данных выполняется векторизованная операция, например:

data$Z <- data$X * data$Y

Будет ли это использовать возможности процессора с одной инструкцией и несколькими данными (SIMD) для оптимизации производительности? Это кажется идеальным случаем для этого, но я не могу найти ничего, что подтверждает мою догадку.

2 ответа

Решение

Я только что получил значок "хороший ответ" через два года после моего первоначального ответа. Спасибо за признание качества этого ответа. Взамен я бы существенно обогатил оригинальное содержание. Этот ответ / статья теперь предназначена для любого пользователя R, который хочет попробовать SIMD. Он имеет следующие разделы:

  • Предыстория: что такое SIMD?
  • Резюме: как мы можем использовать SIMD для нашей сборки или скомпилированного кода?
  • Сторона R: как R может использовать SIMD?
  • Производительность: всегда ли векторный код быстрее скалярного кода?
  • Написание расширений R: пишите скомпилированный код с OpenMP SIMD

Предыстория: что такое SIMD?

Многие программисты на R могут не знать SIMD, если они не пишут ассемблер или скомпилированный код.

SIMD (одна инструкция, несколько данных) - это технология параллельной обработки на уровне данных, которая имеет очень долгую историю. До появления персональных компьютеров SIMD однозначно ссылался на векторную обработку в векторном процессоре и был основным путем к высокопроизводительным вычислениям. Когда персональные компьютеры позже появились на рынке, у них не было функций, напоминающих функции векторного процессора. Однако по мере того, как требования к обработке мультимедийных данных становились все выше и выше, они стали иметь векторные регистры и соответствующие наборы векторных инструкций для использования этих регистров для загрузки векторных данных, арифметики векторных данных и хранения векторных данных. Емкость векторных регистров возрастает, а функциональность наборов векторных инструкций также становится все более универсальной. До сегодняшнего дня они могут выполнять потоковую загрузку / сохранение, пошаговую загрузку / сохранение, разбросанную загрузку / сохранение, перетасовку векторных элементов, векторную арифметику (включая объединенную арифметику, такую ​​как слияние-сложение-сложение), векторные логические операции, маскирование и т. Д. Все больше и больше похожи мини-векторные процессоры старых времен.

Хотя SIMD работает с персональными компьютерами почти два десятилетия, многие программисты не знают об этом. Вместо этого многие знакомы с параллелизмом на уровне потоков, таким как многоядерные вычисления (которые можно назвать MIMD). Поэтому, если вы новичок в SIMD, вам настоятельно рекомендуется посмотреть это видео на YouTube, используя остальные 80% производительности вашей системы: начиная с векторизации Ульриха Дреппера из Red Hat Linux.

Поскольку векторные наборы команд являются расширениями для исходных наборов команд архитектуры, вам необходимо приложить дополнительные усилия для их использования. Если вы хотите написать ассемблерный код, вы можете сразу вызвать эти инструкции. Если вы хотите написать скомпилированный код, такой как C, C++ и Fortran, вы должны написать встроенную сборку или использовать векторные встроенные функции. Встроенная векторная функция выглядит как функция, но на самом деле это встроенное отображение сборки на инструкцию по сборке вектора. Эти встроенные функции (или "функции") не являются частью стандартных библиотек скомпилированного языка; они обеспечиваются архитектурой / машиной. Чтобы использовать их, нам нужно включить соответствующие заголовочные файлы и специфичные для компилятора флаги.

Давайте сначала определим следующее для простоты последующих обсуждений:

  1. Написание ассемблера или скомпилированного кода, который не использует наборы векторных инструкций, называется "написание скалярного кода";
  2. Написание ассемблера или скомпилированного кода, использующего наборы векторных инструкций, называется "написание векторного кода".

Таким образом, эти два пути - "напиши A, получи A" и "напиши B, получи B". Тем не менее, компиляторы становятся сильнее, и существует другой путь "напиши A, получи B":

  1. У них есть возможность преобразовать ваш написанный скалярный скомпилированный код в код векторной сборки - оптимизация компилятора, называемая "векторизация".

Некоторые компиляторы, такие как GCC, рассматривают автоматическую векторизацию как часть оптимизации самого высокого уровня и активируются с помощью флага -O3; в то время как другие более агрессивные компиляции, такие как ICC (компилятор Intel C++) и clang, включили бы его в -O2, Авто-векторизация также может напрямую контролироваться определенными флагами. Например, GCC имеет -ftree-vectorize, При использовании автоматической векторизации рекомендуется дополнительно указывать компиляторам код тейлора для сборки векторов для машины. Например, для GCC мы можем сделать -march=native и для ICC мы используем -xHost, Это имеет смысл, потому что даже в семействе архитектур x86-64 более поздние микроархитектуры имеют больше наборов векторных инструкций. Например, Sandybridge поддерживает наборы векторных инструкций до AVX, haswell также поддерживает AVX2 и FMA3, а Skylake также поддерживает AVX-512. Без -march=native GCC генерирует только векторные инструкции, используя наборы команд до SSE2, который является гораздо меньшим подмножеством, общим для всех x86-64.


Резюме: как мы можем использовать SIMD для нашей сборки или скомпилированного кода?

Существует пять способов реализации SIMD:

  1. Написание машинно-специфического кода векторной сборки напрямую. Например, в x86-64 мы используем наборы инструкций SSE / AVX, а в архитектурах ARM мы используем наборы инструкций NEON.

    • Плюсы: код может быть настроен вручную для лучшей производительности;
    • Минусы: нам приходится писать разные версии кода сборки для разных машин.
  2. Написание векторного скомпилированного кода с использованием машинно-зависимых векторных и компиляции его с помощью флагов, специфичных для компилятора. Например, на x86-64 мы используем встроенные функции SSE / AVX, а для GCC мы устанавливаем -msse2, -mavx и т. д. (или просто -march=native для автоопределения). Вариант этой опции - написать специфическую для компилятора встроенную сборку. Например, введение в встроенную сборку GCC можно найти здесь.

    • Плюсы: написание скомпилированного кода проще, чем написание ассемблерного кода, и код более читабелен, следовательно, проще в обслуживании;
    • Минусы: нам приходится писать разные версии кода для разных машин и адаптировать Makefile для разных компиляторов.
  3. Написание векторного скомпилированного кода с использованием специфичных для компилятора векторных расширений. Некоторые компиляторы определили свой собственный тип векторных данных. Например, векторные расширения GCC можно найти здесь;

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

    • Плюсы: написание скалярного скомпилированного кода проще, чем написание векторного скомпилированного кода, и более читабельно для широкой аудитории.
    • Минусы: мы должны адаптировать Makefile для разных компиляторов, и в случае, если мы использовали прагмы, они также должны быть версионными.
  5. написание скалярного скомпилированного кода и вставка прагм OpenMP (требуется OpenMP 4.0+) #pragma opm simd,

    • Плюсы: аналогично варианту 4 и, кроме того, мы можем использовать одну версию прагм, поскольку многие компиляторы основного потока поддерживают стандарт OpenMP;
    • Минусы: нам нужно адаптировать Makefile для разных компиляторов, так как они могут иметь разные флаги для включения OpenMP и машинной настройки.

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

  • Для вариантов 1 и 2 на x86-64 встроенные функции SSE / AVX, безусловно, являются лучшей эталонной картой, но не подходящим местом для начала изучения этих инструкций. С чего начать - индивидуально. Я взял встроенную сборку / сборку из BLISLab, когда попытался написать свой собственный высокопроизводительный DGEMM (будет представлен позже). После того, как я переварил пример кода там, я начал практиковать и опубликовал несколько вопросов о Stackru или CodeReview, когда застрял.

  • Для вариантов 4 хорошее объяснение дает Руководство по автоматической векторизации с помощью компиляторов Intel C++. Хотя руководство предназначено для ICC, принцип работы автоматической векторизации применим и к GCC. Официальный веб-сайт для векторизации GCC настолько устарел, и этот слайд презентации более полезен: GOV-векторизация.

  • Для варианта 5 имеется очень хороший технический отчет Национальной лаборатории Ок-Риджа: Эффективная векторизация с OpenMP 4.5.

С точки зрения портативности,

  • Опции с 1 по 3 не легко переносимы, потому что версия векторного кода зависит от машины и / или компилятора;

  • Вариант 4 намного лучше, поскольку мы избавляемся от машинной зависимости, но у нас все еще есть проблема с компиляторной зависимостью;

  • Вариант 5 очень близок к переносимому, поскольку адаптировать Makefile намного проще, чем адаптировать код.

С точки зрения производительности, традиционно считается, что вариант 1 является наилучшим, и производительность будет ухудшаться по мере нашего снижения. Однако компиляторы становятся лучше, а на более новых машинах улучшается аппаратное обеспечение (например, снижение производительности для невыровненной векторной нагрузки меньше). Так что векторизация очень позитивна. В рамках моего собственного примера DGEMM я обнаружил, что на рабочей станции Intel Xeon E5-2650 v2 с пиковой производительностью 18 GFLOP на процессор, автоматическая векторизация GCC достигла 14 ~ 15 GFLOP, что довольно впечатляюще.


Сторона R: как R может использовать SIMD?

R может использовать SIMD только путем вызова скомпилированного кода, использующего SIMD. Скомпилированный код в R имеет три источника:

  1. Пакеты R с "базовым" приоритетом, такие как base, stats, utils и т. д., которые поставляются с исходным кодом R;
  2. Другие пакеты на CRAN, которые требуют компиляции;
  3. Внешние научные библиотеки, такие как BLAS и LAPACK.

Поскольку само программное обеспечение R переносимо между архитектурами, платформами и операционными системами, и политика CRAN предполагает, что пакет R будет одинаково переносимым, скомпилированный код в источниках 1 и 2 не может быть записан в коде сборки или компилированном коде, зависящем от компилятора, исключая варианты с 1 по 3 для реализации SIMD. Автоматическая векторизация - это единственный шанс для R использовать SIMD.

Если вы создали R с включенной автоматической векторизацией компилятора, скомпилированный код из источников 1 может использовать SIMD. На самом деле, хотя R написан переносимым способом, вы можете настроить его для своей машины при сборке. Например, я бы сделал icc -xHost -O2 с ICC или gcc -march=native -O2 -ftree-vectorize -ffast-math с GCC. Флаги устанавливаются во время сборки R и записываются в RHOME/etc/Makeconf (в Linux). Обычно люди просто делают быструю сборку, поэтому конфигурации флагов принимаются автоматически. Результат может отличаться в зависимости от вашей машины и вашего компилятора по умолчанию. На машине Linux с GCC флаг оптимизации часто автоматически устанавливается на -O2 следовательно, автоматическая векторизация отключена; вместо этого на компьютере Mac OS X с clang автоматическая векторизация включена в -O2, Поэтому я предлагаю вам проверить свои Makeconf,

Флаги в Makeconf используются при запуске R CMD SHLIB, вызванный R CMD INSTALL или же install.packages при установке пакетов CRAN R, которые нуждаются в компиляции. По умолчанию, если Makeconf говорит, что авто-векторизация выключена, скомпилированный код из источника 2 не может использовать SIMD. Тем не менее, можно переопределить Makeconf флаги, предоставляя пользовательский файл Makevars (например, ~/.R/Makevars на Linux), так что R CMD SHLIB может взять эти флаги и автоматически векторизовать скомпилированный код из источника 2.

BLAS и LAPACK не являются частью проекта R или зеркала CRAN. R просто принимает его как есть и даже не проверяет, является ли он действительным! Например, в Linux, если вы псевдоним вашей библиотеки BLAS для произвольной библиотеки foo.so, R будет "тупо" загружаться foo.so вместо этого при запуске и причинить вам неприятности! Свободные отношения между R и BLAS упрощают связывание различных версий BLAS с R, так что сравнительный анализ различных библиотек BLAS в R становится простым (или, конечно, вам необходимо перезапустить R после обновления связывания). Для пользователей Linux с привилегиями root переключение между различными библиотеками BLAS рекомендуется с помощью sudo update-alternatives --config, Если у вас нет привилегий root, этот поток в Stackru поможет вам: без root-доступа запустите R с настроенным BLAS, когда он связан с эталонным BLAS.

Если вы не знаете, что такое BLAS, вот краткое введение. BLAS первоначально ссылался на стандарт кодирования для векторно-векторных, матрично-векторных и матрично-матричных вычислений в научных вычислениях. Например, было рекомендовано, чтобы общее умножение матрицы на матрицу C <- beta * C + alpha * op(A) %*% op(B) известный как DGEMM. Обратите внимание, что эта операция больше, чем просто C <- A %*% B и мотивация этого дизайна заключалась в максимизации повторного использования кода. Например, C <- C + A %*% B, C <- 2 * C + t(A) %*% B и т. д. все можно вычислить с помощью DGEMM, Реализация модели с использованием FORTRAN 77 предоставляется с таким стандартом для ссылки, и эта библиотека моделей широко известна как эталонная библиотека BLAS. Такая библиотека статична; он призван мотивировать людей настраивать его производительность для любых конкретных машин. Оптимизация BLAS на самом деле очень сложная работа. В конце оптимизации все меняется, кроме пользовательского интерфейса. Т.е. все внутри функции BLAS изменяется, ожидайте, что вы по-прежнему вызываете ее таким же образом. Различные оптимизированные библиотеки BLAS известны как настроенные библиотеки BLAS и включают, например, ATLAS, OpenBLAS или Intel MKL. Все настроенные библиотеки BLAS используют SIMD как часть своей оптимизации. Оптимизированная библиотека BLAS значительно быстрее, чем эталонная, и разрыв в производительности для новых машин будет значительно больше.

R полагается на BLAS. Например, матрично-матричный оператор умножения "%*%" в R позвоню DGEMM, функции crossprod, tcrossprod также сопоставлены с DGEMM, BLAS лежит в центре научных расчетов. Без BLAS R был бы в значительной степени нарушен. Настоятельно рекомендуется связать оптимизированную библиотеку BLAS с R. Раньше было трудно проверить, какая библиотека BLAS связана с R (поскольку это может быть скрыто псевдонимом), но с R 3.4.0 это уже не так., sessionInfo() покажет полные пути к библиотеке или исполняемым файлам, обеспечивающим реализацию BLAS / LAPACK, используемую в настоящее время (недоступно в Windows).

LAPACK - это более продвинутая научная библиотека, построенная на базе BLAS. R полагается на LAPACK для различных матричных факторизаций. Например, qr(, pivot = TRUE), chol, svd а также eigen в R отображаются в LAPACK для QR-факторизации, факторизации Холецкого, разложения по сингулярным числам и разложения по собственным значениям. Обратите внимание, что все настроенные библиотеки BLAS включают клон LAPACK, поэтому, если R связан с настроенной библиотекой BLAS, sessionInfo() покажет, что обе библиотеки идут по одному пути; вместо этого, если R связан с эталонной библиотекой BLAS, sessionInfo() будет иметь два разных пути для BLAS и LAPACK. Было задано много вопросов относительно резких различий в производительности умножения матриц на разных платформах, таких как большие различия в производительности ОС для матричных вычислений. На самом деле, если вы просто посмотрите на вывод sessionInfo() вы сразу получаете подсказку, что R связан с настроенным BLAS на первой платформе и ссылкой BLAS на второй.


Производительность: всегда ли векторный код быстрее скалярного кода?

Векторный код выглядит быстро, но он не может быть реально быстрее, чем скалярный код. Вот пример: почему это умножение SIMD не быстрее, чем умножение не SIMD?, И что за совпадение, векторная операция, исследуемая там, является именно тем, что здесь взяла OP, например: продукт Адамара. Люди часто забывают, что скорость процессора не является решающим фактором для практической производительности. Если данные не могут быть перенесены из памяти в ЦП так же быстро, как запросы ЦП, ЦП будет просто сидеть и ждать большую часть времени. Пример продукта Hadamard просто попадает в эту ситуацию: для каждого умножения из памяти должно быть выбрано три данных, поэтому продукт Hadamard является операцией с привязкой к памяти. Мощь процессора может быть реализована только тогда, когда выполняется существенно больше арифметики, чем количество перемещений данных. Классическое матрично-матричное умножение в BLAS относится к этому случаю, и это объясняет, почему реализация SIMD из настроенной библиотеки BLAS так полезна.

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


Написание расширений R: пишите скомпилированный код с OpenMP SIMD

Если вы решите внести свой вклад в CRAN, написав свои собственные пакеты R, вы можете рассмотреть возможность использования опции 5 SIMD: автоматическая векторизация OpenMP, если какая-то часть вашего скомпилированного кода может извлечь выгоду из SIMD. Причина, по которой не выбран вариант 4, заключается в том, что когда вы пишете распространяемый пакет, вы не представляете, какой компилятор будет использоваться конечным пользователем. Таким образом, вы не можете написать специфичный для компилятора код и опубликовать его в CRAN.

Как мы указывали ранее в списке опций SIMD, использование OpenMP SIMD требует от нас адаптации Makefile. На самом деле, R делает это очень легко для вас. Вам никогда не нужно писать Makefile вместе с пакетом R. Все, что вам нужно, это Makevars файл. Когда ваш пакет компилируется, флаги компилятора указываются в вашем пакете Makevars и RHOME/etc/Makeconf в машине пользователя будет передан R CMD SHLIB, Хотя вы не знаете, какой компилятор может использовать этот пользователь, RHOME/etc/Makeconf знает! Все, что вам нужно сделать, это указать в вашей упаковке Makevars что вы хотите поддержку OpenMP.

Единственное, что вы не можете сделать в вашем пакете Makevars дает подсказку для машинной настройки. Вместо этого вы можете посоветовать пользователям вашего пакета сделать следующее:

  • Если RHOME/etc/Makeconf на машине пользователя уже есть такая конфигурация настройки (то есть пользователь настроил флаги при сборке R), ваш скомпилированный код должен быть преобразован в настроенный код векторной сборки, и больше ничего не нужно делать;
  • В противном случае, вам нужно посоветовать пользователям редактировать там личные Makevars файл (как ~/.R/Makevars в Linux). Вам нужно составить таблицу (возможно, в виньетке вашего пакета или документации) о том, какие флаги настройки должны быть установлены для каких компиляторов. Сказать -xHost для МУС и -march=native для GCC.

Ну, есть малоизвестный дистрибутив R от Microsoft (художник, ранее известный как Revolution R), который можно найти здесь

Он поставляется с библиотекой Intel MKL, которая также использует несколько потоков и векторных операций (хотя вы должны запускать процессор Intel), и он действительно помогает с матрицами, такими как SVD и т. Д.

Если вы не готовы писать код C/C++ с использованием встроенных в SIMD встроенных средств с использованием Rcpp или аналогичных интерфейсов, Microsoft R - ваш лучший выбор для использования SIMD

Скачайте и попробуйте

Другие вопросы по тегам