Несколько слов о диспетчеризации

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

В предыдущем посте я немного рассказал об одном из возможных решений - динамической генерации кода. Это реализовано в Intel Array Building Blocks. У этого подхода свои плюсы и минусы. К плюсам следует отнести универсальность и гибкость, к минусам внутреннюю сложность и значительные накладные расходы (ведь сначала код надо сгенерировать).

Вопрос: какие еще существуют решения для оптимизации кода под различные платформы? Попытаемся разобраться в способе, который называется диспетчеризацией (Dispatching).

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

Исходя из того, что простое лучше, чем сложное, можно сделать несколько вариантов кода, каждый из которых функционально идентичен остальным, но оптимизирован под свой набор процессоров. Очевиден и основной недостаток: на каждом CPU необходимо выбрать, какой процесс запустить или какую библиотеку вызвать. Этот минус можно обойти, если использовать дополнительный модуль. Он вызывается в run-time, автоматически определяет тип процессора и предлагает выбрать ветку кода, которая подойдет под текущее "железо" наилучшим образом. Этот модуль называется диспетчером (Dispatcher).

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

Существуют различные реализации диспетчеров. По одной из них (в библиотеке Intel IPP) диспетчер идентифицирует CPU один раз - во время инициализации библиотеки, после чего задает набор внутренних переменных. В соответствии с этим набором библиотечные вызовы из приложения перенаправляются в соответствующие внутренние функции библиотеки. Например, при запуске приложения на Core 2 Duo в 32-битной операционной системе вызов функции ippsCopy_8u() перенаправляется на p8_ippsCopy_8u() - одну из множества реализаций функциональности ippsCopy_8u(), имеющихся в библиотеке.

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

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