Эльбрус/портирование

Материал из ALT Linux Wiki

Перенос ПО на платформу Эльбрус

Почему? -- на e2k не будут работать программы, скомпилированные для других архитектур, например, x86 или ARM[1]. Зачем? -- см. ЧаВо.

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

В ALT RPM реализован макрос %e2k, рекомендуемый к применению в %ifarch.

Также обратите внимание: прописанные сборочные зависимости далеко не всегда минимальны, порой при сложностях стоит посмотреть, а не лучше ли на первом этапе оторвать (скажем, перенеся под %ifnarch %e2k) необязательную зависимость или отключить "ручку" (--without jit), лишившись части функциональности на конкретной архитектуре, но привнеся остальную часть.

configure: error: cannot guess build type; you must specify one

В архив исходников программы включены устаревшие копии этих файлов, поддержка e2k добавлена в gnu-config в 2015 году; достаточно обновить их вручную из свежей системной версии этого пакета или automake (который с большей вероятностью окажется под рукой) либо выполнить autoreconf -fisv:

cp -aLt . -- /usr/share/automake/config.{guess,sub}
autoreconf -fisv

В %changelog можно добавить, например[2]:

- fix build on newer arches

Если апстрим без GPL3-фобии -- стоит предложить обновить сразу там.

configure: error: invalid value: boost_major_version=''

Препроцессор из lcc 1.25 может добавить лишний пробел в препроцессированный исходник, что ломает тест версии библиотеки boost, входящий в некоторые применяющие autoconf программы с типичной диагностикой (mcst#6826):

checking for Boost's header version...
configure: error: invalid value: boost_major_version=

Обход -- смягчение регулярного выражения перед запуском configure (либо в m4/boost.m4 или ином его источнике перед запуском autoreconf; поправьте команду по месту):

# lcc's cpp adds an extra space breaking this regex
sed -r -i 's,\^boost(.)lib(.)version,boost\1lib\2version,' configure m4/boost.m4

тесты на порядок байтов/битность

Нередко попадаются программы, которые интересует только длина указателей (размер integer) и, возможно, endianness; поскольку e2k -- 64-разрядная LE-архитектура, ищем подстроку вроде __amd64__, читаем контекст, добавляем аналогично __e2k__.

Про невыровненный доступ к памяти на версиях архитектуры до пятой включительно ("Эльбрус-8СВ") известно, что он достаточно дорогой; поэтому про unaligned access интересующемуся коду на <e2kv6 можно сообщить, что таковой отсутствует.

Размер строки кэша L1 -- 32 байта, L2+ -- 64 байта; судя по патчам МЦСТ и поведению -fprefetch, указывать следует именно 64.

Размеры типов можно глянуть в патче к nspr.

cmake

В альтовых пакетах на cmake исправления проверок битности порой выглядят примерно так[3]:

-%ifarch x86_64
+%if "%_lib" == "lib64"
 export LIB_SUFFIX=64
 %endif
- fixed build on 64-bit architectures

boost

В проектах на boost порой попадается тот ax_boost_base.m4, где в проверку на lib64 забит список архитектур; его придётся поправить перед запуском autoreconf[4] как-то так:

%ifarch %e2k ppc64le riscv64 loongarch64
sed -i 's,aarch64,&|e2k|ppc64le|riscv64|loongarch64,' m4/ax_boost_base.m4
%endif

...после чего не забываем autoreconf -fisv (или %autoreconf).

Обратите внимание: характерная диагностика configure: error: Could not link against -l... может наводить на ложный след, если сбоит тест boost (но не приводит к останову), а вываливается тест последующей библиотеки.

В альтовом пакете autoconf-archive это исправлено начиная со сборки 2019.01.06-alt1.1 в p9_e2k и с 2021.02.19-alt1 -- в p10_e2k и sisyphus_e2k.

SIMD

Алгоритм портирования таких программ простой:

  1. ищем в исходниках макрос __x86_64__[5] или на худой конец i386; если они покрывают фрагменты кода с SIMD-интринсиками (функции, имена которых начинаются на _mm_), то нам повезло;
  2. заменяем defined __x86_64__ на defined __x86_64__ || defined __e2k__;
  3. если попадается динамическая проверка наличия MMX/SSE, то указываем, что у нас всё есть до SSE4.1[6];
  4. к asm-вставкам нужно творчески подходить[7], но чаще проще готовый generic-вариант кода использовать.

См. тж. проект SIMD Everywhere.

Примечание: несмотря на некоторую аппаратную поддержку выполнения SIMD-инструкций, по сути они реализуются в компиляторе и осмысленность задействования может отличаться от проекта к проекту -- возможно замедление, особенно на AVX* и архитектурах ранее e2kv5.
Примечание: если собираемый код замечает arm_neon.h и радостно пытается собрать ARM-ассемблер -- сообщите ему в явном виде, что NEON нет: в любом случае этот набор интринсиков хуже конвертируется в итоговые вектора на e2k.


компилятор/архитектура

Имейте в виду при выписывании #ifdef:

Во-первых, lcc есть не только для e2k (привет sparc), поэтому если делается патч под особенности lcc, то правильнее использовать __LCC__ (скорее даже __EDG__ для фронтенда edg, можно проверить на старых ICC тем же Compiler Explorer); обратите внимание, порой уже есть #ifdef __ICC либо #if defined(__INTEL_COMPILER), в каковом случае можно либо пропатчить, расширив список, либо взвести проверяемое:

%add_optflags -D__ICC

Во-вторых, со временем на e2k появляются и другие компиляторы, например, clang через соответствующий бэкенд на основе lcc. И у них уже может не быть макроса __LCC__, а вот __e2k__ есть.

Поэтому мне представляется правильным архитектурно-зависимые изменения в e2k заворачивать, а компиляторо-зависимые в LCC. Понятно, что в реальной жизни их отличить не всегда просто. - @bircoph[8]

отсутствие makecontext()

На Эльбрусах makecontext_e2k() выделяет память под дополнительные стеки, поэтому если просто заменить s/makecontext/makecontext_e2k/, в программе может появиться утечка памяти. Нужно ещё поставить вызов freecontext_e2k() там, где выделенный для makecontext_e2k() ucp.uc_stack перестаёт использоваться под данный контекст, т.е. где:

  1. ucp.uc_stack освобождается через free();
  2. ucp.uc_stack переиспользуется, например, под другой контекст.

Должна стоять проверка на makecontext < 0: makecontext_e2k() возвращает значение int, а не void. Значение вызова необходимо проверять на статус ошибки (< 0).

Если речь про coroutines, надо уходить с fcontext на портабельную вещь, поддерживающую ucontext-e2k (например, koishi).

отсутствие cpuid.h

См. тж. wiki.elbrus.ru/CPU_id, mcst#6511, mcst#8456

Обуславливаем соответствующий #include и обращения к функциям так:

#if defined(__i386__) || defined(__x86_64__)

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

При необходимости подробного различения процессоров "Эльбрус" обратите внимание на __builtin_cpu_is(); в lcc от 1.23.23 и 1.24.10 должны быть доступны более удобные __builtin_cpu_name() и __builtin_cpu_arch() (#4484).

Есть альтернативный способ -- через чтение регистра IDR, что позволяет определить модель процессора если код скомпилирован под другой процессор и даже если недоступен /proc/cpuinfo. Для него написана маленькая библиотека под WTFPL.

rdtsc

Вместо ассемблерных вставок для обращения к TSC применяйте интринсик:

#include <x86intrin.h>
uint64_t time = __rdtsc();
// same: unsigned aux; uint64_t time = __rdtscp(&aux);

SIGILL вместо ожидаемого сигнала

Обратите внимание, что на e2k в некоторых случаях можно получить SIGILL (Illegal instruction) вместо ожидаемого SIGSEGV (Segmentation fault), SIGBUS (Bus error) или SIGFPE (Floating point exception).

Не тот сигнал приходит, как правило, либо в результате работы оптимизаций, либо при ручном написании кода на ассемблере или ассемблерных вставках[9]. Если по каким-то причинам нужно поймать именно тот сигнал, который бы поймался на других архитектурах, то следует использовать режим с отключением оптимизаций, задействующих полуспекулятивный режим исполнения:

  • -O0 -- режим компиляции без оптимизаций;
  • -O1 -- минимальный набор оптимизаций;
  • -fcontrol-spec -- запрет полуспекулятивных обращений к памяти (для сохранения сингалов SIGSEGV и SIGBUS);
  • -fno-fp-spec -- запрет полуспекулятивных вещественных операций (для сохранения сигнала SIGFPE; в lcc >= 1.26 -- -fno-spec-fp).

См. тж. Руководство и posix_signals.html[10].

наивные тесты в cmake

Если в каком-либо проекте на cmake вылезает "неизвестная опция", как в glslang:

if(NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS "9.0.0")
 add_compile_options(-Werror=deprecated-copy)
endif()

-- это прибитая гвоздями зависимость от gcc/clang; нужно:

  1. проверить доступность опции и выставить переменную, см. здесь; пример: раз, два;
  2. добавить опцию, если выставлена переменная; пример.

отсутствие sys/io.h

Данный заголовок существует для небольшого количества архитектур: alpha, arm, ia64, x86, x86_64 и определяет inline-работу с портами ввода-вывода, которые только на них в этом виде и наличествуют. На всех других архитектурах (включая aarch64 и ppc64le) соответствующий код подлежит усечению (при возможности) либо переработке, если на bit banging завязана ключевая функциональность программы.

waf

Если для сборки предлагается старый безэльбрусный waf, добавьте в waflib/Tools/c_config.py строчку

'__e2k__'     : 'e2k',

после описания xtensa.

%set_gcc_version

Альт-специфика: если какой-либо пакет не собирается (или перестал собираться после его обновления) с чем-то вроде

E: Couldn't find package gcc12-c++

-- видимо, майнтейнер обошёл слом сборки этой версии пакета на другой версии gcc в сизифе таким образом; поскольку lcc представляет из себя другой компилятор (и прикидывается, как правило, на несколько версий более старым gcc, чем в сизифе) -- на e2k есть смысл такие ограничения убрать: соберётся и заработает -- хорошо, нет -- ну нет.

На том же примере igraph это выглядит так:

+%ifarch %e2k
+%define gcc_ver %nil
+%else
 %define gcc_ver 12
+%endif
+%if 0%gcc_ver
 export CC=gcc-%gcc_ver
 export CXX=g++-%gcc_ver
+%endif

Ссылки

Примечания

  1. точнее, для x86 есть бинарный транслятор, но это средство совместимости, а не нормальное явление
  2. поскольку затрагивает и riscv64, и обычно aarch64
  3. либо можно задействовать %_libsuff
  4. или найти этот фрагмент в уже сгенерированном configure, что несколько сложней
  5. или же __amd64__
  6. расширения системы команд SSE4.2 и AVX1 в каком-то виде также поддержаны в компиляторе, но, возможно, быстрее не будет
  7. <ilyakurdyukov> Такое:
        inline int getCSR()
        {
            int result;
            asm volatile("stmxcsr %0" : "=m" (result));
            return result;
        }
    
        inline void setCSR(int a)
        {
            int temp = a;
            asm volatile("ldmxcsr %0" : : "m" (temp));
        }
    

    лучше делать так:

        inline int getCSR() { return _mm_getcsr(); }
    
        inline void setCSR(int a) { _mm_setcsr(a); }
    

    но надо добавить заголовок для работы интринсиков:

    #include <xmmintrin.h>
    
  8. соображения коллег из МЦСТ (#8192#22):
    lcc: __EDG__ __GNUC__ __LCC__ __MCST__
    clang: __clang__ __LCC__ __MCST__
    gcc: __GNUC__ __MCST__

    ...плюс архитектурнозависимый
  9. Пример недопустимых инструкций: чтение с помощью apb по невыровненному адресу (причём проявляется только при попытке использовать значение в другой инструкции), попытка записи диагностического значения в память или использование регистра с диагностическим значением не в спекулятивном режиме. Обычно это всё невозможно отследить до исполнения (например, адрес приходит как аргумент функции). -- Дмитрий Щербаков
  10. в составе lcc1.25-doc или аналогичного пакета на ОС Альт, по пути /opt/mcst/doc/posix_signals.html при установленной системе программирования в Эльбрус Линукс