2016-06-06 7 views
35

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

Это, по-видимому, означает, что «стиль кодирования» влияет на производительность, то есть использование именованных переменных для хранения результатов промежуточных вычислений может отрицательно повлиять на производительность, заставив некоторые части вычисления вычисляться "слишком рано".

Чтобы попытаться проверить свою интуицию, я написал пример и был удивлен результатами (full code here):

using ArrayXf = Eigen::Array <float, Eigen::Dynamic, Eigen::Dynamic>; 
using ArrayXcf = Eigen::Array <std::complex<float>, Eigen::Dynamic, Eigen::Dynamic>; 

float test1(const MatrixXcf & mat) 
{ 
    ArrayXcf arr = mat.array(); 
    ArrayXcf conj = arr.conjugate(); 
    ArrayXcf magc = arr * conj; 
    ArrayXf mag = magc.real(); 
    return mag.sum(); 
} 

float test2(const MatrixXcf & mat) 
{ 
    return (mat.array() * mat.array().conjugate()).real().sum(); 
} 

float test3(const MatrixXcf & mat) 
{ 
    ArrayXcf magc = (mat.array() * mat.array().conjugate()); 

    ArrayXf mag  = magc.real(); 
    return mag.sum(); 
} 

выше дает 3 различные способы вычисления коэффициента мудр сумма величин в комплекснозначная матрица.

  1. test1 вид берет каждую порцию вычисления «один шаг за раз».
  2. test2 делает все вычисление в одном выражении.
  3. test3 принимает «смешанный» подход - с некоторым количеством промежуточных переменных.

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

Однако результаты оказались неожиданными (номера указаны в общих микросекунд по 1000 выполнений каждого теста): (. Это был составлен с г ++ -O3 - см the gist полную информацию)

test1_us: 154994 
test2_us: 365231 
test3_us: 36613 

Версия, которую я ожидал быть самой быстрой (test2), была фактически самой медленной. Кроме того, версия, которую я ожидал быть самой медленной (test1), была фактически посередине.

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

  1. Почему test3 выполнять намного лучше, чем альтернативные варианты?
  2. Есть ли способ, которым можно использовать (не считая погружения в код сборки), чтобы получить представление о том, как Eigen фактически выполняет ваши вычисления?
  3. Есть ли набор руководящих принципов, которые следует соблюдать, чтобы найти хороший компромисс между производительностью и читабельностью (использование промежуточных переменных) в вашем Eigen-коде?

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

+3

Я не эксперт по оптимизации, но я бы с подозрением относился к вашим результатам, учитывая, что вы компилировали с '-O3' и не фиксировали ни один из результатов вычислений. Вполне возможно, что оптимизатор распознает отсутствие побочных эффектов 'funcN()' и оптимизирует весь расчет. Я считаю, что вы можете использовать 'volatile' для поддержки микро-бенчмаркинга. [релевантный вопрос SO] (http://stackoverflow.com/questions/6130100/using-volatile-to-prevent-compiler-optimization-in-benchmarking-code) –

+0

Обратите внимание, что с недавним компилятором программа прерывается все время , Он передается только с более старыми компиляторами, потому что версия 'abs', которая вызывается, представляет собой целочисленную версию ... –

ответ

13

Похоже, проблема GCC. Ожидается, что компилятор Intel даст ожидаемый результат.

$ g++ -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -o a && ./a 
test1_us: 200087 
test2_us: 320033 
test3_us: 44539 

$ icpc -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -o a && ./a 
test1_us: 214537 
test2_us: 23022 
test3_us: 42099 

По сравнению с версией icpc, gcc, кажется, проблема оптимизации test2.

Для более точного результата вы можете отключить утверждения отладки на -DNDEBUG, как показано на рисунке here.

EDIT

На вопрос 1

@ggael дает отличный ответ, что gcc терпит неудачу векторизации петлю суммы. Мой эксперимент также обнаружил, что test2 работает так же быстро, как рукописный наивный для цикла, как с gcc, так и с icc, что указывает на то, что причиной является векторизация, а временное выделение памяти не обнаружено в test2 по методу, упомянутому ниже, что указывает на то, что Eigen правильно оцените выражение.

На вопрос 2

Избежание промежуточной памяти является основной целью, что Эйген использовать шаблоны выражений. Таким образом, Eigen предоставляет макрос EIGEN_RUNTIME_NO_MALLOC и простую функцию, позволяющую вам проверить, выделена ли промежуточная память при вычислении выражения. Вы можете найти пример кода here. Обратите внимание, что это может работать только в режиме отладки.

EIGEN_RUNTIME_NO_MALLOC - если он определен, новый переключатель вводится который можно включать и выключать с помощью вызова set_is_malloc_allowed (логическое значение).Если malloc не разрешен, и Eigen пытается распределить память динамически в любом случае, результаты отказа подтверждения. Не определено по умолчанию.

На вопросе 3

Существует способ использовать промежуточные переменные, и получить повышение производительности введенного ленивыми шаблонов оценки/экспрессии одновременно.

Способ использования промежуточных переменных с правильным типом данных. Вместо использования Eigen::Matrix/Array, который инструктирует выражение для оценки, вы должны использовать тип выражения Eigen::MatrixBase/ArrayBase/DenseBase, чтобы выражение было буферизировано, но не оценивалось. Это означает, что вы должны хранить выражение как промежуточное, а не результат выражения, при условии, что этот промежуточный элемент будет использоваться только один раз в следующем коде.

Как определение параметров шаблона в выражении типа Eigen::MatrixBase/... может быть болезненным, вместо этого вы можете использовать auto. Вы можете найти некоторые подсказки, когда вам следует/не следует использовать auto/типы выражений в this page. Another page также расскажет вам, как передать выражения в виде функциональных параметров без их оценки.

В соответствии с поучительным экспериментом около .abs2() в ответе @ggael, я думаю, что еще одно руководство - избежать повторного использования колеса.

+0

Спасибо за ответ! Я изначально думал об использовании 'auto' в том, как вы описываете, но меня испугало« не использовать «auto» ... если вы не уверены в том, что вы делаете ». :-) – jeremytrimble

+2

'auto' также должен буферизовать промежуточные продукты. – Yakk

+0

Их следует использовать только для буферизации промежуточных продуктов, которые вы не хотите оценивать. – kangshiyin

0

Я просто хочу, чтобы вы заметили, что вы сделали профилирование неоптимальным способом, поэтому на самом деле проблема может быть просто вашим профилирующим методом.

Поскольку Есть много вещей, как кэш местность, чтобы держать во внимание, вы должны сделать профилирование таким образом:

int warmUpCycles = 100; 
int profileCycles = 1000; 

// TEST 1 
for(int i=0; i<warmUpCycles ; i++) 
     doTest1(); 

auto tick = std::chrono::steady_clock::now(); 
for(int i=0; i<profileCycles ; i++) 
     doTest1(); 
auto tock = std::chrono::steady_clock::now(); 
test1_us = (std::chrono::duration_cast<std::chrono::microseconds>(tock-tick)).count(); 

// TEST 2 


// TEST 3 

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

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

Также вы должны попробовать разные компиляторы, чтобы увидеть, является ли проблема разворачиванием шаблонов (существует ограничение на глубину для оптимизации шаблонов: возможно, вы можете поразить его одним большим выражением).

Кроме того, если семантика поддержки поддержки Eigen не имеет причины, почему одна версия должна быть быстрее, поскольку не всегда гарантируется, что выражения могут быть оптимизированы.

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

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

+1

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

13

Что происходит, так это то, что из-за шага .real() Eigen явно не выделяет вектор test2. Таким образом, это вызовет стандартный оператор complex :: operator *, который, к сожалению, никогда не привязан gcc. Другие версии, с другой стороны, используют собственную реализацию векторизованных продуктов Eigen.

Напротив, ICC выполняет встроенный комплекс :: operator *, что делает test2 самым быстрым для ICC. Вы также можете переписать test2 как:

return mat.array().abs2().sum(); 

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

gcc: 
test1_us: 66016 
test2_us: 26654 
test3_us: 34814 

icpc: 
test1_us: 87225 
test2_us: 8274 
test3_us: 44598 

clang: 
test1_us: 87543 
test2_us: 26891 
test3_us: 44617 

Чрезвычайно высокую оценку МТП в данном случае связано с его умной автоматической векторизации двигателя.

Другой способ обхода отката gcc без изменения test2 - это определить свой собственный operator* для complex<float>. Например, добавьте следующую строку в верхней части файла:

namespace std { 
    complex<float> operator*(const complex<float> &a, const complex<float> &b) { 
    return complex<float>(real(a)*real(b) - imag(a)*imag(b), imag(a)*real(b) + real(a)*imag(b)); 
    } 
} 

, а затем я получаю:

gcc: 
test1_us: 69352 
test2_us: 28171 
test3_us: 36501 

icpc: 
test1_us: 93810 
test2_us: 11350 
test3_us: 51007 

clang: 
test1_us: 83138 
test2_us: 26206 
test3_us: 45224 

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

+0

Итак, кажется, что моя интуиция была бы правильной, за исключением gcc quirk. Спасибо за ответ. К моменту моих двух других вопросов, есть ли какие-либо трюки, которые вы предложите, чтобы получить представление о том, как Эйген решил оптимизировать выражение? Кроме того, есть ли способ разбить вычисление на множество подвыражений, не мешая ленивой оценке (например, могу ли я объявить 'arr',' conj', 'magc' и т. Д. Как какой-то другой тип, чтобы оценка могла произойти позже)? – jeremytrimble

+0

Почему '.real()' не будет векторизован, недостаток в библиотеке? – Yakk

+3

С '-fcx-limited-range' (включенным в' -ffast-math') или '-fcx-fortran-rules', gcc будет встроить сложное умножение. Icc по умолчанию небезопасный режим, сомнительный выбор ... –

4

Одна вещь, которую я сделал раньше, - это использовать ключевое слово auto. Помня о том, что большинство выражений Eigen возвращают типы данных специальных выражений (например, CwiseBinaryOp), назначение обратно на Matrix может заставить выражение оцениваться (это то, что вы видите).Использование auto позволяет компилятору вывести тип возвращаемого значения, как любой тип выражения оно, что позволит избежать оценки как можно дольше:

float test1(const MatrixXcf & mat) 
{ 
    auto arr = mat.array(); 
    auto conj = arr.conjugate(); 
    auto magc = arr * conj; 
    auto mag = magc.real(); 
    return mag.sum(); 
} 

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

Смежные вопросы