Зависимый от процессора код: как избежать указателей на функции? - PullRequest
4 голосов
/ 11 марта 2012

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

void do_something_neon(void);
void do_something_armv6(void);

void (*do_something)(void);

if(cpu == NEON) {
    do_something = do_something_neon;
}else{
    do_something = do_something_armv6;
}

//Use function pointer:
do_something(); 
...

Не то, чтобы это имело значение, но я упомяну, что я оптимизировал функции для разных процессоров: armv6 и armv7 с поддержкой NEON. Проблема в том, что при использовании указателей на функции во многих местах код замедляется, и я бы хотел избежать этой проблемы.

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

Лично я бы предложил два разных способа избежать указателей на функции: создать два отдельных .so (или .dll) для функций, зависящих от процессора, поместить их в разные папки и, основываясь на обнаруженном ЦП, добавить одну из этих папок в поиск. путь (или LD_LIB_PATH). Загрузите основной код и динамический компоновщик выберет необходимые DLL из пути поиска. Другой способ - скомпилировать две отдельные копии библиотеки :) Недостаток первого метода заключается в том, что он заставляет меня иметь как минимум 3 общих объекта (dll): два для функций, зависящих от процессора, и один для основного кода, который их использует. Мне нужно 3, потому что я должен быть в состоянии обнаружить процессор перед загрузкой кода, который использует эти зависимые от процессора функции. Хорошая часть первого метода заключается в том, что приложению не нужно загружать несколько копий одного и того же кода для нескольких процессоров, оно будет загружать только ту копию, которая будет использоваться. Недостаток второго метода вполне очевиден, об этом говорить не нужно.

Я хотел бы знать, есть ли способ сделать это без использования общих объектов и загрузки их вручную во время выполнения. Одним из способов может быть хакерство, которое включает в себя исправление кода во время выполнения, вероятно, это слишком сложно, чтобы сделать это правильно). Есть ли лучший способ контролировать перемещения во время загрузки? Может быть, разместить зависимые от процессора функции в разных разделах, а затем как-то указать, какой раздел имеет приоритет? Я думаю, что в мачо-формате MAC есть что-то подобное.

Мне достаточно решения только для ELF (для мишени), я не очень люблю PE (dll's).

спасибо

Ответы [ 4 ]

7 голосов
/ 11 марта 2012

Вы можете посмотреть расширение динамического компоновщика GNU STT_GNU_IFUNC.Из блога Дреппера, когда он был добавлен:

Поэтому я разработал расширение ELF, которое позволяет принимать решение о том, какую реализацию использовать один раз за запуск процесса.Это реализовано с использованием нового типа символа ELF (STT_GNU_IFUNC).Всякий раз, когда поиск символа преобразуется в символ с этим типом, динамический компоновщик не сразу возвращает найденное значение.Вместо этого он интерпретирует значение как указатель на функцию, которая не принимает аргументов и возвращает указатель реальной функции для использования.Вызываемый код может находиться под контролем разработчика и может выбирать, основываясь на любой информации, которую хочет использовать разработчик, какую из двух или более реализаций использовать.*

Тем не менее, как уже говорили другие, я думаю, вы ошибаетесь по поводу влияния косвенных вызовов на производительность.Весь код в разделяемых библиотеках будет вызываться через (скрытый) указатель функции в GOT и запись PLT, которая загружает / вызывает этот указатель функции.

4 голосов
/ 11 марта 2012

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

Я бы предложил сделать следующее:

  1. перемещение большей части кода основного исполняемого файла, который часто вызывает функции DLL, в DLL.Это сведет к минимуму количество косвенных вызовов в секунду и позволит улучшить оптимизацию также во время компиляции.
  2. перемещение почти всего вашего кода в отдельные специфичные для ЦП библиотеки DLL и оставление функции main () только загружатьправильная DLL ИЛИ создание специфичных для CPU исполняемых файлов без DLL.
2 голосов
/ 02 июня 2012

Вот точный ответ, который я искал.

 GCC's __attribute__((ifunc("resolver")))

Для этого требуются сравнительно свежие binutils Есть хорошая статья, которая описывает это расширение: Поддержка Gnu для диспетчеризации процессора - вроде ...

0 голосов
/ 12 марта 2012

Ленивая загрузка символов ELF из общих библиотек описана в разделе 1.5.5 Инструкции DSO Ульриха Дреппера (обновлено 2011-12-10). Для ARM это описано в разделе 3.1.3 ELF для ARM .

РЕДАКТИРОВАТЬ: С расширением STT_GNU_IFUNC, упомянутым R. Я забыл, что это расширение. GNU Binutils поддерживает это для ARM, по-видимому, с марта 2011 года, согласно changelog .

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

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

...