Как в бинарнике организована поддержка различных процессоров?
От: elmal  
Дата: 19.11.19 12:10
Оценка: :)
Заинтересовал меня один вопрос.

Сейчас бешенное количество процессорных архитектур. И даже в рамках x86-64 архитектуры постоянно добавляют новые команды, всякие AVX, AVX2 и т.д. И возникает вопрос, как компилятор (для простоты возьмем gcc) компилирует код таким образом, чтобы максимально использовались возможности новых процессоров, но была возможность запустить код на старых процессорах без поддержки новых инструкций.

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

Так как это все сделано?
Re: Как в бинарнике организована поддержка различных процесс
От: mike_rs Россия  
Дата: 19.11.19 12:19
Оценка:
Здравствуйте, elmal, Вы писали:

E>Так как это все сделано?

в общем случае никак. Использование расширений архитектуры задается ключами компилятора, которые ты сам как разработчик можешь задать любыми. Например, если ты уверен, что программа работает на нужно процессоре. если не уверен — то можно руками проверить в рантайме и слинковать с двумя разными версиями модуля. Например в случае неких стронних либ, сделано именно так — несколько вариантов кода и внутри в рантайме выбирается самый быстрый.
Причем не надо иметь поддержку всех возможных вариантов железа — достаточно один универсальный, но медленный, который работает всегда и один или несколько оптимизированных, например для SSE2 или еще чего, и все.
Или еще вариант — используешь либу с быстрым кодом, на старте программы проверяешь фичи процессора, если не подходит — сообщение с просьбой запустить другой бинарник и завершение работы.
Отредактировано 19.11.2019 12:21 mike_rs . Предыдущая версия . Еще …
Отредактировано 19.11.2019 12:20 mike_rs . Предыдущая версия .
Re: Как в бинарнике организована поддержка различных процессоров?
От: Bill Baklushi СССР  
Дата: 19.11.19 12:24
Оценка:
elmal:

E>Самый простой вариант как это решить я вижу следующий — компилировать код под все возможные варианты, и в самом начале программы команда cpuid и jmp на код, соответствующий наиболее оптимальному выполнению. Но что то у меня сомнения что это делается именно так, ибо код раздувается сразу раз в 10 минимум и будет далее раздуваться еще, когда этих расширений станет еще больше. Плюс сильно растет время компиляции.


Всё просто. Пользовательские программы дёргают высокоуровневые API, процессорозависимые функции выносятся в драйверы.
Объединяйтесь, либералы, для рытья каналов!
Re: Как в бинарнике организована поддержка различных процессоров?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 19.11.19 12:53
Оценка:
Здравствуйте, elmal, Вы писали:

E>компилировать код под все возможные варианты, и в самом начале программы команда cpuid и jmp на код, соответствующий наиболее оптимальному выполнению.


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

Главное перед всем этим — определить, насколько такая оптимизация реально необходима. Если увеличение быстродействия будет меньше, чем на полтора-два десятка процентов, особого смысла нет.
Re[2]: Как в бинарнике организована поддержка различных процессоров?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 19.11.19 12:54
Оценка: +2
Здравствуйте, Bill Baklushi, Вы писали:

BB>процессорозависимые функции выносятся в драйверы.


Таки не в драйверы, а в библиотеки.
Re[2]: Как в бинарнике организована поддержка различных процессоров?
От: elmal  
Дата: 19.11.19 12:57
Оценка:
Здравствуйте, Bill Baklushi, Вы писали:

BB>Всё просто. Пользовательские программы дёргают высокоуровневые API, процессорозависимые функции выносятся в драйверы.

А библиотеки как компилировать? Хорошо, основная программа у меня не использует никаких расширений, чистый i386. Но как мне добиться чтобы программа работала как на i386, так и использовала MMX, SSE, SSE2, AVX и т.д в зависимости от того, под каким процом запущена?
Re[3]: Как в бинарнике организована поддержка различных процессоров?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 19.11.19 13:05
Оценка:
Здравствуйте, elmal, Вы писали:

E>А библиотеки как компилировать?


Что значит "как"? Путем запуска компилятора, вестимо.

E>Но как мне добиться чтобы программа работала как на i386, так и использовала MMX, SSE, SSE2, AVX и т.д в зависимости от того, под каким процом запущена?


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

То, что у Вас эта задача вызывает такое непонимание, говорит о том, что Вы довольно слабо понимаете суть процессов компиляции, линковки, взаимодействия кода и т.п., очень советую разобраться лучше.
Re[4]: Как в бинарнике организована поддержка различных процессоров?
От: elmal  
Дата: 19.11.19 13:46
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>Элементарно. Основной код компилируется под i386. Вынесенные в библиотеки модули независимо (например, с помощью макросов и переименования) компилируются в различные файлы, для соответствующих расширений. Дальше все это линкуется, и основной код по селектору вызывает нужные функции.

Итого. Предположим у меня критический модуль ровно один matrixUtils.so (dll) с одной функцией matrixMultiply. Я хочу здесь использовать все возможности своего железа. Сделал эту функцию вообще средствами OpenMP чтоб у меня распараллелилось все само, я только всякие pragma расставил, чтоб я не на ассемблере все сам хреначил. Как эта либа будет собрана в бинарнике?
Лет 20 назад у меня вариантов было немного, я бы зафигачил все на ассемблере для каждого из возможных вариантов оптимизация (там собственно MMX и SSE), и в основной функции у меня бы стояла CPUID и я бы далее в зависимости от типа процессора делегировал бы соответствующей оптимизированной ассемблерной функции. Но мне все таки казалось, что за 20 лет сейчас компиляторы поумнели, и на ассемблере мне все фигачить уже не надо, тем более всяких векторных инструкций сейчас столько расплодилось, что вообще хрен разберешься.
Re: Как в бинарнике организована поддержка различных процессоров?
От: Nuzhny Россия https://github.com/Nuzhny007
Дата: 19.11.19 14:06
Оценка: 11 (2)
Здравствуйте, elmal, Вы писали:

E>Так как это все сделано?


Могу рассказать про то, как это устроено в OpenCV. Там есть 2 базовых макроса: CPU_DISPATCH и CPU_BASELINE. CPU_DISPATCH отвечает за то, какие архитектуры поддерживаются, а CPU_BASELINE — начиная с какой. Например CPU_BASELINE=SSE2, CPU_DISPATCH=SSE2,SSE3,SSE4.1,AVX,AVX2. Это всё настраивается в CMake.
Далее эти макросы определяются детальней в коде, где каждая из архитектур в CPU_DISPATCH прячется в виде набора интрисиков. С помощью каждого из набора интрисиков оптимизируются функции для обработки изображэений: ресайз, конвертация цвета и т.д. и т.п. В общем всё, что может быть оптимизировано имеет по несколько реализаций: просто на С/С++, на каждом из набора интрисиков, на OpenCL, на OpenVX... При запуске приложения проверяется тип процессора и какой набор инструкций поддерживается. В зависимости от этого внутри общей функции, например, cv::resize вызывается конкретная реализация, оптимальная для текущего процессора. Все реализации живут в одной dll/so. В коде библиотеки это выглядит страшно и слабо читаемо, но хорошо скрыто от пользователя и работает быстро.
https://elibrary.ru/author_counter.aspx?id=875549
Re[5]: Как в бинарнике организована поддержка различных процессоров?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 19.11.19 15:04
Оценка:
Здравствуйте, elmal, Вы писали:

E>Как эта либа будет собрана в бинарнике?


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

E>казалось, что за 20 лет сейчас компиляторы поумнели, и на ассемблере мне все фигачить уже не надо


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

Если GCC поддерживает управление кодогенерацией через pragma или подобные средства — еще проще. Заворачиваете функцию в шаблон с параметром, и несколько раз специализируете его в тексте одного и того же модуля, переключая режимы кодогенерации. Вызываете, соответственно, шаблоны с явными параметрами.
Re[2]: Как в бинарнике организована поддержка различных процессоров?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 19.11.19 15:07
Оценка:
Здравствуйте, Nuzhny, Вы писали:

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


Хватает стандартных intrinsic'ов, или какие-то компиляторы позволяют определять их самостоятельно?
Re[5]: Как в бинарнике организована поддержка различных процессоров?
От: andrey.desman Россия  
Дата: 19.11.19 15:25
Оценка:
Здравствуйте, elmal, Вы писали:

ЕМ>>Элементарно. Основной код компилируется под i386. Вынесенные в библиотеки модули независимо (например, с помощью макросов и переименования) компилируются в различные файлы, для соответствующих расширений. Дальше все это линкуется, и основной код по селектору вызывает нужные функции.

E>Итого. Предположим у меня критический модуль ровно один matrixUtils.so (dll) с одной функцией matrixMultiply. Я хочу здесь использовать все возможности своего железа. Сделал эту функцию вообще средствами OpenMP чтоб у меня распараллелилось все само, я только всякие pragma расставил, чтоб я не на ассемблере все сам хреначил. Как эта либа будет собрана в бинарнике?

Да как обычно... Собираешь ее с поддержкой нужных тебе фич, линкуешь к универсальному бинарю, потом в рантайме смотришь на каком камне все крутится и
auto matrixMultiply = cpuOk ? &optMatrixMultiply : &slowMatrixMultiply;
auto m = matrixMultiply(m1, m2);
Re[2]: Как в бинарнике организована поддержка различных процессоров?
От: lpd Россия  
Дата: 19.11.19 15:31
Оценка:
Здравствуйте, Nuzhny, Вы писали:

N> При запуске приложения проверяется тип процессора и какой набор инструкций поддерживается. В зависимости от этого внутри общей функции, например, cv::resize вызывается конкретная реализация, оптимальная для текущего процессора. Все реализации живут в одной dll/so. В коде библиотеки это выглядит страшно и слабо читаемо, но хорошо скрыто от пользователя и работает быстро.


Это понятно, так везде. Но вопрос вроде как в том, чтобы компилятор сам автоматом создавал варианты функций на С(не на ассемблере вручную), оптимизированные под разные наборы инструкций, и вызывал нужную автоматом в зависимости от текущего cpu, без программированния. Не думаю, впрочем, что это такая необходимая фича.
- Сколько тролля ни корми, все равно флеймить будет.
— Заставь тролля C++ учить, он и последний стандарт применит.
Re[3]: Как в бинарнике организована поддержка различных процессоров?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 19.11.19 17:41
Оценка: +1
Здравствуйте, lpd, Вы писали:

lpd>Но вопрос вроде как в том, чтобы компилятор сам автоматом создавал варианты функций на С(не на ассемблере вручную), оптимизированные под разные наборы инструкций, и вызывал нужную автоматом в зависимости от текущего cpu, без программированния.


Если компилятор вдруг стал бы делать это автоматом, то контролировать это стало бы гораздо сложнее, чем делать руками, и вдобавок генерировалось бы много лишнего и малопонятного кода, что идет вразрез с принципами C/C++. Если хочется максимального автоматизма — есть виртуальные функции.
Re: Как в бинарнике организована поддержка различных процессоров?
От: Pzz Россия https://github.com/alexpevzner
Дата: 19.11.19 20:15
Оценка:
Здравствуйте, elmal, Вы писали:

E>Сейчас бешенное количество процессорных архитектур. И даже в рамках x86-64 архитектуры постоянно добавляют новые команды, всякие AVX, AVX2 и т.д. И возникает вопрос, как компилятор (для простоты возьмем gcc) компилирует код таким образом, чтобы максимально использовались возможности новых процессоров, но была возможность запустить код на старых процессорах без поддержки новых инструкций.


Никак.

E>Так как это все сделано?


Компилятору говорят опциями, под какой процессор компилировать, и на более младшем процессоре программа не запустится или упадет. В библиотеке некоторые функции могут явно ветвиться на одну из нескольких имплементаций, в зависимости от процессора.
Re: Как в бинарнике организована поддержка различных процессоров?
От: ononim  
Дата: 19.11.19 20:53
Оценка: 2 (1)
E>Так как это все сделано?
Помимо всякого рассказанного в контексте топика стоит упомянуть о таком механизме в лялихах как GNU Indirect Function.
Этот механизм позволяет либе выбирать в рантайме какую реализацию функции стоит выставить в белый свет под заданным именем.
  както так
/lib/x86_64-linux-gnu$ readelf --dyn-syms libc-2.23.so |grep IFUNC
29: 00000000000bc460 169 IFUNC GLOBAL DEFAULT 13 __gettimeofday@@GLIBC_2.2.5
93: 000000000008b160 53 IFUNC GLOBAL DEFAULT 13 strcpy@@GLIBC_2.2.5
115: 00000000000ac850 55 IFUNC GLOBAL DEFAULT 13 wmemcmp@@GLIBC_2.2.5
178: 000000000008bb20 65 IFUNC GLOBAL DEFAULT 13 strncmp@@GLIBC_2.2.5
312: 000000000008f970 53 IFUNC WEAK DEFAULT 13 stpncpy@@GLIBC_2.2.5
370: 0000000000116300 104 IFUNC GLOBAL DEFAULT 13 __mempcpy_chk@@GLIBC_2.3.4
399: 000000000008d3c0 53 IFUNC GLOBAL DEFAULT 13 strncpy@@GLIBC_2.2.5
440: 00000000000bc380 169 IFUNC GLOBAL DEFAULT 13 time@@GLIBC_2.2.5
533: 000000000008d6f0 34 IFUNC GLOBAL DEFAULT 13 strpbrk@@GLIBC_2.2.5
560: 000000000008da80 34 IFUNC GLOBAL DEFAULT 13 strspn@@GLIBC_2.2.5
621: 000000000008f970 53 IFUNC GLOBAL DEFAULT 13 __stpncpy@@GLIBC_2.2.5
752: 000000000008fa00 84 IFUNC GLOBAL DEFAULT 13 __strcasecmp@@GLIBC_2.2.5
846: 000000000008f1b0 65 IFUNC GLOBAL DEFAULT 13 memset@@GLIBC_2.2.5
865: 000000000008e630 33 IFUNC GLOBAL DEFAULT 13 strstr@@GLIBC_2.2.5
869: 000000000008b280 34 IFUNC GLOBAL DEFAULT 13 strcspn@@GLIBC_2.2.5
893: 000000000008ebb0 55 IFUNC GLOBAL DEFAULT 13 memcmp@@GLIBC_2.2.5
938: 000000000008f330 104 IFUNC WEAK DEFAULT 13 mempcpy@@GLIBC_2.2.5
1006: 0000000000091ca0 65 IFUNC GLOBAL DEFAULT 13 __strncasecmp_l@@GLIBC_2.2.5
1132: 00000000000943f0 106 IFUNC GLOBAL DEFAULT 13 memcpy@@GLIBC_2.14
1134: 000000000008f14b 87 IFUNC GLOBAL DEFAULT 13 memcpy@GLIBC_2.2.5
1156: 00000000001162a0 87 IFUNC GLOBAL DEFAULT 13 __memmove_chk@@GLIBC_2.3.4
1201: 0000000000089880 53 IFUNC GLOBAL DEFAULT 13 strcat@@GLIBC_2.2.5
1206: 000000000008fa00 84 IFUNC WEAK DEFAULT 13 strcasecmp@@GLIBC_2.2.5
1267: 000000000008f330 104 IFUNC GLOBAL DEFAULT 13 __mempcpy@@GLIBC_2.2.5
1273: 00000000000f6a50 33 IFUNC GLOBAL DEFAULT 13 __sched_cpucount@@GLIBC_2.6
1391: 00000000000abc80 35 IFUNC GLOBAL DEFAULT 13 wcscpy@@GLIBC_2.2.5
1397: 0000000000116370 65 IFUNC GLOBAL DEFAULT 13 __memset_chk@@GLIBC_2.3.4
1458: 0000000000089a80 34 IFUNC WEAK DEFAULT 13 index@@GLIBC_2.2.5
1485: 0000000000091cf0 84 IFUNC WEAK DEFAULT 13 strncasecmp@@GLIBC_2.2.5
1507: 000000000008bae0 53 IFUNC GLOBAL DEFAULT 13 strncat@@GLIBC_2.2.5
1593: 00000000000bc460 169 IFUNC WEAK DEFAULT 13 gettimeofday@@GLIBC_2.2.5
1637: 0000000000116210 104 IFUNC GLOBAL DEFAULT 13 __memcpy_chk@@GLIBC_2.3.4
1720: 0000000000089a80 34 IFUNC GLOBAL DEFAULT 13 strchr@@GLIBC_2.2.5
1789: 000000000008f850 53 IFUNC GLOBAL DEFAULT 13 __stpcpy@@GLIBC_2.2.5
1878: 0000000000091ca0 65 IFUNC WEAK DEFAULT 13 strncasecmp_l@@GLIBC_2.3
1935: 000000000008f9b0 65 IFUNC GLOBAL DEFAULT 13 __strcasecmp_l@@GLIBC_2.2.5
2001: 000000000008f9b0 65 IFUNC WEAK DEFAULT 13 strcasecmp_l@@GLIBC_2.3
2003: 000000000008f14b 87 IFUNC GLOBAL DEFAULT 13 memmove@@GLIBC_2.2.5
2078: 000000000008ebb0 55 IFUNC WEAK DEFAULT 13 bcmp@@GLIBC_2.2.5
2104: 0000000000089cd0 53 IFUNC GLOBAL DEFAULT 13 strcmp@@GLIBC_2.2.5
2136: 000000000008f850 53 IFUNC WEAK DEFAULT 13 stpcpy@@GLIBC_2.2.5
Как много веселых ребят, и все делают велосипед...
Re[2]: Как в бинарнике организована поддержка различных процессоров?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 19.11.19 21:02
Оценка:
Здравствуйте, ononim, Вы писали:

O>стоит упомянуть о таком механизме в лялихах как GNU Indirect Function.

O>Этот механизм позволяет либе выбирать в рантайме какую реализацию функции стоит выставить в белый свет под заданным именем.

Он по удобству использования и объему кода/данных чем-то отличается от указателя на функцию и таблицы/селектора адресов функций?
Re[3]: Как в бинарнике организована поддержка различных проц
От: ononim  
Дата: 19.11.19 21:10
Оценка:
O>>стоит упомянуть о таком механизме в лялихах как GNU Indirect Function.
O>>Этот механизм позволяет либе выбирать в рантайме какую реализацию функции стоит выставить в белый свет под заданным именем.
ЕМ>Он по удобству использования и объему кода/данных чем-то отличается от указателя на функцию и таблицы/селектора адресов функций?
Это и есть селектор адресов функций. Профит получается от того что про него знает dynamic linker. То есть при линковки апликухи к библиотеке dynamic линкер не берет просто поинтер на функцию из dynamic symbols, а вызывает библиотеку, и та сообщает ему поинтер. В результате
1) Нулевой оверхед (кроме времени рантайм линковки) по сравнению с выбором указателя на функцию каждый раз внутри библиотеки
2) Нулевой геморрой пользователя библиотеки. Он просто вызывает memcpy, без предварительных присядяний типа p_memcpy = select_best_memcpy();
Как много веселых ребят, и все делают велосипед...
Отредактировано 19.11.2019 21:12 ononim . Предыдущая версия . Еще …
Отредактировано 19.11.2019 21:10 ononim . Предыдущая версия .
Re[4]: Как в бинарнике организована поддержка различных проц
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 19.11.19 21:21
Оценка:
Здравствуйте, ononim, Вы писали:

O>1) Нулевой оверхед (кроме времени рантайм линковки) по сравнению с выбором указателя на функцию каждый раз внутри библиотеки


То есть, в каждой точке вызова используется непосредственный переход, а не косвенный? А линковщик кладет в исполняемый файл таблицу всех этих точек, и каждую перенастраивают в рантайме? Тогда это дает оверхед в виде таблиц, плюс необходимость объявлять эти страницы read-write. Оно того стоит?
Re[5]: Как в бинарнике организована поддержка различных проц
От: ononim  
Дата: 19.11.19 21:28
Оценка:
O>>1) Нулевой оверхед (кроме времени рантайм линковки) по сравнению с выбором указателя на функцию каждый раз внутри библиотеки

ЕМ>То есть, в каждой точке вызова используется непосредственный переход, а не косвенный?

Там по любому косвенный переход на адрес из PLT/GOT. Профит в том что этот переход один.

ЕМ>А линковщик кладет в исполняемый файл таблицу всех этих точек, и каждую перенастраивают в рантайме? Тогда это дает оверхед в виде таблиц, плюс необходимость объявлять эти страницы read-write. Оно того стоит?

А он это всегда делает. Я про runtime dynamic linker, который в винде называется PE loader. Не путай с линкером который для сборки испольхуется.
Как много веселых ребят, и все делают велосипед...
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.