[Перевод] Инструменты Intel для оптимизации приложений и задача о течениях в пористых средах

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

Оптимизации, которые влияют на скорость вычислений при использовании любого числа потоков, сводятся к учёту в коде особенностей процессорных архитектур, наборов инструкций, к поиску наиболее рациональных способов работы с разными видами памяти. Помощь в подобной оптимизации способен оказать, например, подход к исследованию производительности методом «сверху вниз» с использованием низкоуровневых данных о системных событиях, которые преобразуются в доступные для анализа и практического применения высокоуровневые показатели.

8e03897c1065404f8f078d27114dd3a2.jpg

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

f1d3331f12e617a5037d63b7803d1056.jpg

Моделирование течений многофазных жидкостей в пористых средах

Данная задача находит применение во многих сферах. Например, в области добычи нефти и газа, в промышленности, экологии.

Мы рассмотрим методики векторизации кода, расскажем о переходе от последовательного к параллельному исполнению, проведём ряд экспериментов. Эксперименты проводились на платформах Intel, для анализа и оптимизации приложения применялись различные программные средства, в частности, Intel VTune Amplifier.

Материал состоит из трёх основных разделов:

  • В первом мы поговорим об автоматической векторизации кода и проведём эксперименты на системе с процессором Intel Core i5–4350U в однопоточном режиме.
  • Второй раздел посвящён сравнению однопоточного и двухпоточного исполнения приложения. Здесь мы будем сравнивать скорость работы разных вариантов программы на процессоре Intel Core i7–5500U.
  • Третий раздел посвящён подготовке приложения для исполнения в многопоточной среде. Он включает в себя рассказ об анализе и оптимизации приложения с применением Intel Parallel Advisor. Испытывать код будем на узле вычислительного кластера с 4-мя Intel Xeon E5–2698 v3.

Анализ и автоматическая векторизация кода


▍Испытательная платформа и набор инструкций Intel AVX2


Здесь, в качестве аппаратной платформы для экспериментов, используется система на Ubuntu 14.04 с установленным процессором Intel Core i5–4350U (1.4 ГГц, 2 ядра, 4 потока) с 3 Мб кэш-памяти и 4 Гб RAM. Наша экспериментальная среда построена на процессоре, принадлежащем к семейству Intel Haswell, которое отличается полной поддержкой набора инструкций Intel AVX2. Использование возможностей этого набора инструкций, в частности, улучшенная поддержка векторизации, позволяет повысить производительность приложений, предназначенных для широкого спектра платформ, среди них — серверные решениях на базе Intel Xeon и Intel Xeon Phi.

При разработке для Intel AVX2 используется та же модель программирования, что использовалась в предыдущей версии, Intel AVX, однако, новые инструкции расширяют то, что было, предлагая большинство 128-битных целочисленных SIMD инструкций с возможностью обработки 256-битных чисел. Векторизация кода позволяет раскрыть потенциал однопоточного варианта программы, что крайне важно перед переходом к следующему шагу — параллельному исполнению.

Для анализа производительности и оценки качества оптимизации здесь использована Intel Parallel Studio XE 2015.

▍О выявлении потенциала оптимизации


Для того, чтобы понять, как оптимизировать код, нужно его проанализировать и найти блоки программы, исполнение которых можно ускорить за счёт векторизации и параллелизации. Для того, чтобы этого достичь, можно воспользоваться инструментами Intel, такими, как Intel VTune Amplifier и Intel Parallel Advisor. Весьма полезны в этом плане и отчёты, которые генерирует компилятор.

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

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

Перед программистом, который занимается оптимизацией кода, стоит непростая задача. Главная сложность здесь в том, что ему нужно как можно эффективнее использовать аппаратные возможности компьютера, а они находятся на уровне микроархитектуры вычислительной системы, который расположен довольно далеко от уровня абстракции, предоставляемого современными языками программирования, такими, как C, C++ и Fortran.

Инструменты Intel могут помочь в оптимизации кода, анализируя компиляцию и компоновку приложения, давая сведения о временных параметрах исполнения программы для определения того, как именно используются аппаратные ресурсы. Среди этих инструментов — отчёты компилятора, Intel VTune, Intel Parallel Advisor. Вот как можно организовать работу с ними.

  • Анализ отчёта компилятора. Компилятор способен генерировать отчёт, из которого можно узнать, какие автоматические оптимизации были применены к коду, а какие — нет. Иногда, например, некий цикл может быть автоматически векторизован, а иногда этого не происходит. При этом и в том и в другом случае компилятор сообщает об этом, а подобные сведения полезны для программиста, так как они позволяют ему решить, нужно ли его вмешательство в код или нет.
  • Анализ с использованием Intel VTune Amplifier. Это средство позволяет наблюдать за тем, как приложение исполняется на конвейере, как оно использует память. Кроме того, с помощью VTune Amplifier можно узнать, какое время занимает исполнение каждого модуля, и то, насколько эффективно при этом задействуются ресурсы системы. Intel VTune Amplifier создаёт высокоуровневую систему показателей, с помощью которых можно всё это анализировать. Среди таких показателей, например, количество тактовых циклов, необходимых на инструкцию.
  • Анализ с помощью Intel Parallel Advisor. Этот инструмент может спрогнозировать ожидаемый рост производительности при переходе к параллелизму на уровне потоков. Кроме того, Parallel Advisor способно помочь в подготовке кода к параллельному исполнению.

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

▍Автоматическая векторизация кода


Современные компиляторы способны векторизовать исполняемый код. В частности, для этого устанавливают параметр оптимизации в значение -O2 или выше. Отключить автоматическую векторизацию можно, используя параметр компилятора –no-vec. Это может быть полезно для сравнения производительности разных вариантов исполняемого кода.

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

for (i=0; i<=MAX; i++) {
       c[i] = b[i] + a[i];
}

Если этот цикл не подвергся векторизации, то использование регистров при его исполнении выглядит примерно так, как показано на рисунке ниже.
OcbdpuYkWeCBCHvLyyqptWogm62BVhJZb7B-KavN

Использование регистров при исполнении цикла, который не подвергся векторизации

Если тот же самый цикл векторизовать, будут задействованы дополнительные регистры, как результат, можно будет выполнять четыре операции сложения, используя одну инструкцию. Это называют «один поток команд — много потоков данных» (Single Instruction Multiple Data, SIMD).

DB84vb8fd0NpxG7wYjQHZYKrZ7fu4-9NbINMJT1g

Исполнение векторизованного цикла

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

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

Параметр -xHost указывает компилятору на то, что он должен генерировать исполняемый код, соответствующий архитектуре системы, на которой выполняется компиляция. В случае, когда такой системой является компьютер с процессором семейства Haswell, использование этого параметра гарантирует применение инструкций Intel AVX2.

На рисунке ниже показан сводный отчёт Intel VTune Amplifier по программе, скомпилированной без использования параметра -xHost. Здесь время исполнения составило 186,6 секунд, количество тактовых циклов на инструкцию (Clocks per Instruction, CPI) — примерно 0.5, число выполненных инструкций — 1.067.768.800.000.

sVv1eCsxgkH49w9arOxkjezul7wfHUud5VNQ69HQ

Отчёт VTune Amplifier, параметр -xHost не используется

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

На рисунке ниже показан список модулей приложения, отсортированный по эффективно используемому машинному времени.

wB4y7n1ihpXTIE0ZlBmyxfjW5_HAkQM4vnPsKtAp

Список модулей приложения

Обратите внимание на то, что функция vizinhanca создаёт максимальную нагрузку на систему, используя порядка 20% времени процессора. Эта функция занята пошаговым расчётом параметров пористости, проницаемости, скоростей, потока в каждом элементе и во всех его соседних элементах в моделируемой среде.

Обратите внимание на то, что CPI очень плох у функции fkt, которая занята расчётом параметров потока на заданном шаге по времени. Эта функция вызывает функцию vizinhanca.

Вот фрагмент исходного кода функции visinhanca. Здесь имеются сведения о времени, необходимом на выполнение каждой строки и выведен код на ассемблере для выделенной строки, которая занята линейной реконструкцией. Обратите внимание на то, что в коде нет инструкций Intel AVX2.

GJ1J8EfoF7pOarNY-I1AlNG_fhVTPD71yhPrIcxY

Фрагмент ассемблерного кода линейной реконструкции

В этом фрагменте кода можно видеть следующие инструкции: MULSD, MOVSDQ, SUB, IMUL, MOVSXD, MOVL, MOVQ, ADDQ. Все они из набора инструкций Intel Streaming SIMD Extensions 2 (Intel SSE2). Эти «старые» инструкции появились с выходом в ноябре 2000-го года процессора Intel Pentium 4.

Скомпилировав тот же самый код с параметром –xHost, который указывает компилятору на то, что нужно сгенерировать исполняемый код для архитектуры используемой системы, и проанализировав его исполнение с помощью Intel VTune Amplifier, мы получили следующее.

wGxdmIfbVxIk3jd_amWoxM9D8tZWBStKDLG8QGtG

Отчёт VTune Amplifier, параметр -xHost используется

А именно, время исполнения кода теперь составило 65.6 секунд, CPI — 0.62, что гораздо лучше, чем было раньше и ближе к идеальному значению 0.75.

Для того, чтобы понять причину подобного роста производительности, можно взглянуть на ассемблерный код. Там обнаружатся инструкции из набора команд Intel AVX2. Кроме того, сравнивая отчёты VTune Amplifier, можно заметить, что при использовании Intel AVX2 число выполненных инструкций гораздо меньше, чем в случае с применением SSE2 в невекторизованной версии кода.

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

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

v-d109TrmBzWa9_A2G5Uneh30TML0dctuSkHWcyg

Фрагмент векторизованного ассемблерного кода линейной реконструкции

Обратите внимание на то, что большинство инструкций здесь — векторные, из набора Intel AVX2, а не из SSE2. Имена векторных инструкций начинаются с префикса V.

Например, использование инструкции VMOV вместо MOV указывает на то, что мы имеем дело с векторизованным кодом.

На рисунке ниже вы можете видеть сравнение регистров для наборов инструкций Intel SSE2, AVX и AVX2.

e7LHwpQMADsp4McFdYxbw3rpPjS6U056lsdfaFzH

Регистры и наборы инструкций

Простой в применении подход с включением автоматической векторизации позволяет достичь трёхкратного роста производительности.

▍Поиск проблемных участков кода


В системе, с которой мы экспериментируем, на двух физических ядрах могут исполняться 4 потока. Сейчас единственное сделанное улучшение кода — это автоматическая векторизация, выполненная компилятором. Мы пока далеки от программы, которая способна работать на пределе возможностей системы, код ещё можно оптимизировать. Для того, чтобы продолжить улучшение кода, нужно выявить его узкие места. Например, в исходном варианте нашего приложения имеется множество строк, где используется деление.

Опция –no-prec-div указывает компилятору на то, что операции деления нужно заменить на операции умножения на обратные величины. Этой возможностью мы воспользуемся в ходе оптимизации нашего приложения.

Вот фрагмент исходного кода, где часто используется операция деления. Этот код находится внутри функции udd_rt, которая, по данным ранее проведённого анализа, представляет собой одно из узких мест.

21dgfv0m-Kt6Hw8QZsLUe4McRjG3YinoSP_cFrwu

Множество операций деления в важном участке кода

Однако, у опции –no-prec-div имеется одна проблема. Иногда после этой оптимизации полученные значения не так точны, как при использовании обычного деления по стандарту IEEE. Если для целей приложения важна точность, используйте эту опцию для отключения оптимизации, что приведёт к более точным результатам с некоторым падением производительности.

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

С использованием опций –ipo, –xHost и –no-prec-div общее время исполнения программы снизилось ещё сильнее, до 53.5 секунд.

PJuA-CqPAVbnCFxCnyRq3XW3gHjzCJos6hxkZ3ra

Результат использования опций –ipo, -xHost и –no-prec-div

Для того, чтобы лучше понять вклад в производительность программы, который вносит использование опции –no-prec-div, было проведено испытание с отключением этой опции. В результате время исполнения выросло до 84.324 с. Это означает, что данная опция даёт примерно 1.5-кратный прирост производительности.

Ещё одно испытание, на этот раз с отключённой опцией –ipo, показало, что время исполнения возросло до 69.003 с. Это позволяет говорить о том, что данная опция даёт примерно 1.3-кратный прирост производительности.

▍Использование Intel VTune Amplifier General Exploration для выявления ограничений процессора и памяти


Intel VTune Amplifier может не только сообщать о времени, затраченном на исполнение программы, о CPI, и о том, сколько инструкций было выполнено. Используя его профиль General Exploration, можно получить сведения о поведении программы внутри конвейера процессора и выяснить, можно ли улучшить производительность, если учесть ограничения процессора и памяти.

Ограничения вычислительного ядра (Core Bounds) относятся к проблемам, не связанным с передачей данных в памяти и воздействуют на производительность при исполнении инструкций с изменением их порядка или при перегрузке операционных блоков. Ограничения памяти (Memory Bounds) способны вызвать неэффективную работу конвейера из-за необходимости ожидания завершения инструкций загрузки/сохранения данных. Вместе два этих параметра определяют то, что называют Back-End Bounds, этот показатель можно воспринимать как ограничения, возникающие внутри конвейера.

Если же простои конвейера вызваны незаполненными слотами, то есть, тем фактом, что микрокоманды не успевают поступать в него по каким-либо причинам, и часть конвейера не выполняет полезной работы только потому что ей нечего обрабатывать, перед нами уже так называемые Front-End Bounds — внешние по отношению к конвейеру ограничения.

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

ZCpMUsdWDlzp9wmRBGGyQevRyvXmZkPr6RMKm95_

Конвейер процессора Intel, поддерживающий исполнение инструкций с изменением их порядка

Внутренние структуры, back-end конвейера, представляют собой порты для доставки инструкций к ALU, к блокам загрузки и сохранения. Несбалансированное использование этих портов обычно плохо сказывается на производительности, так как некоторые блоки могут либо простаивать, либо использоваться не так равномерно, как другие, которые оказываются перегруженными.

Для приложения, которое мы здесь рассматриваем, анализ показал, что даже при использовании автоматической векторизации, выполняемой компилятором, имеются проблемы при исполнении кода. А именно — высокое значение показателя Core Bound из-за несбалансированного распределения инструкций по портам.

Gt3v3DF_8oGah2zsYM4h3ulXYk8F8fbGcauGJw95

Ограничения вычислительного ядра

Результаты анализа, показанные на рисунке выше, указывают на высокий уровень тактовых циклов (0.203, или 20.3%), в которых не задействован ни один порт. Это говорит о неэффективном использовании ресурсов процессора. Кроме того, заметен высокий уровень циклов (0.311, или 31.1%), когда задействованы три или более порта. Такая ситуация приводит к перегрузке. Очевидно, перед нами несбалансированное использование ресурсов.

Результаты анализа ситуации, выполненные в Intel VTune Amplifier и показанные на рисунке ниже, позволяют обнаружить причины вышеописанных проблем. В данном случае видны четыре функции (fkt, udd_rt, xlo, и xmax_speed), у которых слишком высок показатель Back-End Bound. Кроме того, функция xmax_speed отличается слишком высоким уровнем неправильных предсказаний (показатель Bad Speculation). Не всё благополучно и в модуле transport.

zac8l1Abw7SDJYxGF47Nmg1ilUv2pGkTVLf3csUp

Анализ показателей Back-End Bound и Bad Speculation

Следующий шаг наших изысканий заключается в использовании Intel VTune Amplifier в качестве инструмента для анализа выявленных проблем. Мы собираемся тщательно изучить каждую отмеченную выше функцию для того, чтобы найти участки кода, из-за которых процессорное время тратится впустую.

Анализируя каждую функцию, которая имеет высокий показатель в графе Back-End Bound, можно выяснить, где именно возникают проблемы. На нескольких рисунках, приведённых ниже, можно видеть, что Intel VTune Amplifier показывает исходный код и соответствующие метрики для каждой строки. Среди них — успешно завершённые инструкции (Retiring), ошибки предсказаний (Bad Speculation), такты (Clockticks), CPI, ограничения внутри конвейера (Back-End Bound). В общем случае, высокое значение показателя Clockticks указывает на ответственные строки кода и VTune выделяет соответствующие им значения.

Вот анализ функции transport, отличающейся высоким показателем Back-End Bound.

wTQ8lG4_-mzgN9dBnCoM3IQvBTgYcjepftJZk1te

Анализ причин высокого значения показателя Back-End Bound в функции transport

Функцию fkt так же можно отнести к разряду ответственных. Она вызывает функции g и h для вычисления потока и функцию udd_rt для вычисления насыщения начального элемента сетки. Аналитические данные по этой функции показаны на рисунке ниже.

-WyH3ouLFTf81ZYwECKO52nAEJvYVH2p7DkE7wF7

Анализ функции fkt

Вот код функций g и h, которые вызывает функция fkt.

function g(uu,vv,xk,VDISPY,POROSITY)
!       
!	
вычисляет f в узле или объёме
!  
use mPropGeoFisica, only: xlw,xlo,xlt,gf2,rhoo,rhow

implicit none
real(8) :: g,uu,vv,xk
REAL(8) :: VDISPY,POROSITY
real(8) :: fw
real(8) :: xlwu,xlou
real(8) :: xemp,xg

xlwu=xlw(uu)
xlou=xlo(uu)
fw=xlwu/(xlwu+xlou)
xg = gf2
xemp=fw*xg*xlou*xk*(rhoo-rhow)
g=fw*vv-xemp

end function

function h(uu,vv,xk,VDISPZ,POROSITY)
!       
!	
вычисляет f в узле или объёме
!  
use mPropGeoFisica, only: xlw,xlo,xlt,gf3,rhoo,rhow

implicit none
real(8) :: h,uu,vv,xk
REAL(8) :: VDISPZ,POROSITY
real(8) :: fw
real(8) :: xlwu,xlou
real(8) :: xemp,xg

xlwu=xlw(uu)
xlou=xlo(uu)
fw=xlwu/(xlwu+xlou)
xg = gf3
xemp=fw*xg*xlou*xk*(rhoo-rhow)
h=fw*vv-xemp

end function

Здесь важно обратить внимание на то, что здесь идёт речь о двумерных компонентах вектора F, который представляет поток сохраняющейся величины s в соответствии со следующим выражением.
8FXkYT0g98JiNmhUaDTt831HHHI6k8-4ts8fMNyq

Функция xlo, которая находит общую подвижность, в соответствии с подвижностью воды и нефти, так же отличается высоким показателем Back-End Bound. Вот её анализ.
cvqVfME5d9fTed0L6PyIMoo3Cd9H0AYCaAYR6fO5

Функция xlo

Вот анализ функции xmax_speed, которая вычисляет максимальную скорость в направлении x. Эта функция так же вносит немалый вклад в общий уровень показателя Back-End Bound.

7hJUOfaH0biPTMd5gKt0f_TYya--LLinYfZyA4tS

Функция xmax_speed

Intel VTune Amplifier использует данные о функциях, анализ которых мы провели выше, для нахождения итогового показателя Back-End Bound.

Как уже было сказано, причиной возникновения такого рода ограничений может являться неэффективное использование процессора или памяти. Так как автоматическая векторизация уже была применена к нашему коду, весьма вероятно, что причина возникшей ситуации — в памяти. Дальнейший анализ, а именно, рассмотрение показателя Memory Bound, данные по которому приведены ниже, позволяет говорить о том, что программа неэффективно использует кэш первого уровня (показатель L1 Bound).

tWc5erU96346L-nIrIChuibQvmvvsLub60DJ6eIE

Показатель Memory Bound

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

▍Применение отчётов компилятора для поиска дополнительных путей оптимизации


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

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

remark #####: loop was not vectorized: inner loop was already vectorized

В некоторых случаях компилятор указывает разработчику на то, что для векторизации цикла ему следует воспользоваться директивой SIMD, но, когда это сделано, цикл всё равно не векторизуется из-за того, что в нём выполняется присвоение скалярной переменной:
!DIR$ SIMD
      do n=1,nstep+1
      ss=smin+(n-1)*ds

Вот соответствующее сообщение компилятора:
LOOP BEGIN at ./fontes/transporte.F90(143,7)
     remark #15336: simd loop was not vectorized: conditional 
     assignment to a scalar   [ ./fontes/transporte.F90(166,23) ]
     remark #13379: loop was not vectorized with "simd"

Большая часть циклов была автоматически векторизована компилятором, и в некоторых случаях потенциальный рост производительности, оценённый компилятором, варьировался от 1.4 (большинство циклов) до 6.5 (такой показатель получен лишь для одного цикла).

Когда компилятор обнаруживает, что после векторизации производительность падает (то есть, рост производительности составляет меньше, чем 1.0), векторизацию он не проводит. Вот один из примеров сообщения компилятора в подобной ситуации.

LOOP BEGIN at ./fontes/transporte.F90(476,7)
      remark #15335: loop was not vectorized: vectorization 
	
possible but seems inefficient. Use vector always directive 
	
or -vec-threshold0 to override 
      remark #15451: unmasked unaligned unit stride stores: 1 
      remark #15475: --- begin vector loop cost summary ---
      remark #15476: scalar loop cost: 3 
      remark #15477: vector loop cost: 3.000 
      remark #15478: estimated potential speedup: 0.660 
      remark #15479: lightweight vector operations: 2 
      remark #15481: heavy-overhead vector operations: 1 
      remark #15488: --- end vector loop cost summary ---
LOOP END

Реально же производительность после автоматической векторизации выросла примерно в три раза, в соответствии с оценённым компилятором средним значением.

Анализ причин, по которым компилятор не выполняет векторизацию некоторых циклов, следует провести до применения других методов оптимизации.

▍Эксперименты с различными размерами сетки


Все тесты, которые проводились до настоящего момента, выполнялись в применении к решению задачи, пространство которой представляет собой сетку размером 100×20x100, при этом проводится 641 шаг вычислений с использованием одного вычислительного потока.

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

Сетка
Время исполнения без векторизации
Время исполнения с векторизацией
Рост производительности
Количество шагов
100×20×100
186,628
53,590
3,4
641
50×10×50
17,789
5,277
3,4
321
25×5×25
0,773
0,308
2,5
161
10×2×10
0,039
0,0156
2,5
65

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

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

Переход от одного потока к двум


▍Испытательная платформа


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

Тесты, о которых здесь пойдёт речь, выполнены на системе, которая отличается от той, с которой мы экспериментировали ранее. На этом компьютере установлена Ubuntu 14.04.3 LTS, он оснащён процессором Intel Core i7–5500U (2.4 ГГц, 4 Мб кэш-памяти) с двумя ядрами и с поддержкой технологии HT (до четырёх одновременно исполняемых потоков). Этот процессор принадлежит к семейству Intel Broadwell, он полностью поддерживает набор инструкций Intel AVX2. В компьютере имеется 7.7 Гб оперативной памяти.

Процессоры семейства Broadwell производятся с использованием 14-нанометрового техпроцесса, развивая микроархитектуру Haswell. В стратегии «тик-так» («tick-tock»), которую использует Intel, это семейство относится к фазе «тик», то есть, это шаг, на котором уменьшен техпроцесс и произведены некоторые улучшения микроархитектуры, представленной ранее.

▍Однопоточное исполнение


Для анализа производительности программы воспользуемся Intel VTune XE 2016. Первый эксперимент заключается в однопоточном исполнении кода, скомпилированного с использованием параметров –ipo, –xHost и –no-prec-div. Такие настройки соответствуют тем, которые использовались ранее.

Расчёты с выполнением 641 шага для той же самой сетки размером 100×20x100, заняли 39.259 секунд, что означает рост производительности в сравнении с процессором семейства Haswell примерно в 1.4 раза. Вот часть отчёта об этом эксперименте из Intel VTune Amplifier.

UNAnZ-vgkMqsg1HXLwiB4iVTSOnBVzf8_5EQWpY_

Отчёт Intel VTune Amplifier

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

Из отчёта видно, что показатель Back-End Bound оставляет 0.375. Это указывает на то, что даже увеличение тактовой частоты процессора не спасает в ситуации, когда причины неэффективного использования ресурсов системы не устранены.

На следующем рисунке показаны развёрнутые данные по показателю Back-End Bound.

2RAyEAmXf21OEF38xeysaPsYFNkx811uJ-1zNvnA

Анализ отчёта Intel VTune Amplifier

Здесь видно, что причиной высокого значения обобщённого показателя Back-End Bound являются ограничения ядра (показатель Core Bound), а именно — задержки в блоке деления (Divider). Есть и другие проблемы, не связанные с блоком деления, на них указывает показатель Port Utilization. Этот показатель позволяет понять, насколько эффективно используются порты исполнения внутри ALU. Высокое значение Core Bound — это отражение последовательности запросов к модулю исполнения или следствие недостатка параллелизма на уровне инструкций в коде. Задержки в работе процессора могут указывать, кроме прочего, на неоптимальное использование порта исполнения. Например, операция деления, довольно ресурсоёмкая, может задержать исполнение, увеличивая потребность в порте, который обслуживает специфические типы микрокоманд. Это, как следствие, это приведёт к малому числу портов, задействованных в тактовом цикле.

Вышесказанное указывает на то, что код должен быть улучшен для того, чтобы избежать зависимости от данных. Здесь нужно применить более эффективный подход, нежели уже использованная автоматическая векторизация.
На рисунке ниже показан один из этапов анализа причин высокого значения показателя Back-End Bound, в частности, функции, которые вносят наибольший вклад в падение производительности, находятся в верхней части списка и выделены. Мы уже сталкивались с этими функциями.

AHZGcDfHVyv2du6RjkukEWMd4349VfAl4hTfVplR

Анализ в Intel VTune Amplifier

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

▍Исполнение с использованием двух потоков


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

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

Вот сводный отчёт Intel VTune после проведения эксперимента.

VDOmdB3YutTay1A3M8bCDBt0-WkNZ5lvx8sh3OJr

Отчёт Intel VTune

Здесь исполняется тот же код, который мы рассматривали выше, единственная разница заключается в том, что переменная среды OMP_NUM_THREADS была установлена в значение 2. Код работает примерно в 1.8 раза быстрее, но показатель Back-End Bound всё ещё высок и у данной ситуации те же причины, о которых мы говорили выше.

Вот подробный анализ происходящего. Здесь видны те же узкие места, что и ранее, к ним, кроме того, добавился высокий уровень параметра Bad Speculation у функции xlo и высокий уровень показат

© Habrahabr.ru