1) несколько раз сюда писал про эту тему, тем кто в курсе - освежить вопрос, тем кто нет - мое оформленое опытом мнение и понятное описание, что зачем и почему.
2) riscv устроен очень интересно и не так как другие процессоры - в нем вообще может не быть регистров.... он вероятно не сможет работать но он будет "вполне себе правильным" с точки зрения архитектуры riscv, Кроме "обычных" регистров так же в этой архитектуре предусматриваются не обычные регистры - регистры CSR ( регистры управления и статуса ), они необычны тем что расположены в отдельном адресном пространстве и их набор весьма кастомизирован... чтоб автор реализации микроархитектуры мог добавить в процессор не нарушая стандарта описания riscv свои фичи и расширения (vendor-specific). Фактически это можно рассматривать как регистры периферии процессора, адресуемые специфическим набором команд.
3) Из обычных регистров условно считается что обязательными должны быть регистры 0, 1, 2, 3 и 4 ...- то есть $zero, $ra, $sp, $gp, ...
можно сказать что регистра $sp может и не быть - скорее всего возможно построить НЕ-стековую машину которая будет выполнять код - линейный код. и она будет riscv машиной! Возможно она не будет сильно полезной.
4) тоже самое можно сказать про регистр $gp - про него и будем вести речь в этом посте. наконец то про главное.
как было сказано - этот регистр тоже может и не быть реализован. и это опять же не криминал - все будет в рамках закона riscv
более того - подозреваю что 90% кода скомпилированного под riscv НЕ использует его, и вполне хорошо живет.
5) как это работает:
riscv RW-операция ячейки памяти (глобальной переменной С/С++ кода) требует двух инструкций:
lui (load upper immediate) загружает старшие биты 32-битного адреса памяти.
lw / sw (load/store sord) выполняет саму операцию со смещением.
Для оптимизации этого процесса используется регистр глобального указателя — $gp (global pointer). Если Вы каким либо способом записали в регистр $gp адрес центра специальной области памяти (small data area). Вы сможете для данных в этой области использовать инструкции типа lw/sw c 12-битовым знаковым смещением (в пределах ±2048 байт от адреса в $gp), к любой переменной внутри этой зоны в 4 КБ можно обратиться всего за одну инструкцию.
Для автоматизации этого вопроса, заставив компилятор и линкер работать в связке, мы получим немного педалей чтоб это разрулить.
Переменные размером меньше или равные размеру указанному ключем компиляции байт -msmall-data-limit=xxx
gcc ...... -msmall-data-limit=1000 .....
компилятор отправляет в секции .sdata (инициализированные "маленькие") и .sbss (неинициализированные "маленькие"). Доступ к ним происходит быстро через регистр $gp.
ch.peripheral_address = &i2c.data ;
92a: 40005337 lui t1,0x40005
92e: 425f8023 sb t0,1056(t6)
932: 41030793 addi a5,t1,1040 # 40005410 <__main_stack_end__+0x1fffd410>
936: 00f8a423 sw a5,8(a7)
tx_state = tx_state_t::idle ;
93a: a601a623 sw zero,-1428(gp) # 2000026c <display::ch32vxxx_i2c_ssd130x_io_t::tx_state>
Переменные больше уходят в стандартные .data и .bss, и доступ к ним требует стандартных двух инструкций.
_ZN8ch32v3xx25dma1_channel6_irq_handlerEv: 000009d0: addi sp,sp,-16 000009d2: sw t0,12(sp) 000009d4: lui a5,0x40020 000009d8: lui a4,0x200 000009dc: sw a4,4(a5)
если учесть что "маленьких" данных может быть много, а операции с ними интенсивными - то использования $gp дает существенное снижение объема кода и его скорости одновременно. не будем забывать что в классических стековых машинах количество инструкций реальной полезной программы может доходить до 60%!!!!!! посмотрите результат дизассемблирования случайного кода для x86_64 - ужаснетесь: в основном пересылки между памятью и регистрами.
6) это была теория, но как это заставить работать на практике? как сгенерить правильный код и управлять этим процессом?
конечно нам помогут компилятор и линкер, но без наших усилий ничего не получится.
при компиляции, как уже было сказано - данные меньше -msmall-data-limit=xxx будут помечены что из нужно положить в бинарь по адресам секций .sdata .sbss
на данном этапе еще не известно где это будет лежать, компилятор не знает об этом ничего. но уже понятно в каких секциях!
далее в работу вступает линкер
для того что бы можно было организовать эту "волшебную область памяти ±2048 байт" необходимо сказать линкеру где она находится, это делается скриптом линкера, в котором указано размещение секций с этими специфическими названиями .sdata .sbss
...
MEMORY { FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K SRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 32K } __main_stack_end__ = ALIGN(ORIGIN(SRAM) + LENGTH(SRAM),8); __main_stack_start__ = __crt_unhundled_end__ ; __main_stack_size__ = __main_stack_end__ - __main_stack_start__ ; __global_pointer$ = ORIGIN(SRAM) + 2K; __$gp_low_bound__ = __global_pointer$ - 2K; __$gp_high_bound__ = __global_pointer$ + 2K;
...
/* первые 4K sram, допускают быстрое обращение через $gp для тонкого управления - помещаем первыми - секции пользователя .sudata, subss, затем атоматически размещаемые компилятором .sdata и .sbss */ /*------- small user data & bss section( .sudata, .subss ) -------------------------------------------------------------------------*/ __sudata_load_start__ = LOADADDR(.sudata); .sudata : { __sudata_start__ = . ; *(.sudata .sudata* .gnu.linkonce.sud.*) /* секция инициализированных данных "small"*/ . = ALIGN(4); __sudata_end__ = .; } >SRAM AT>FLASH __sudata_load_end__ = __sudata_load_start__ + SIZEOF(.sudata); .subss (NOLOAD): { __subss_start__ = __sudata_end__ ; *(.subss .subss* gnu.linkonce.sub.*) /* секция не ининициализированных данных "small" */ . = ALIGN(4) ; __subss_end__ = .; } >SRAM /*------- standart C/C++ Run-Time RISCV (CRT-RISCV) compiler-generated small sdata & sbss section( .sdata, .sbss ) ----------------------------------------------*/ __sdata_load_start__ = LOADADDR(.sdata); .sdata : { __sdata_start__ = __subss_end__; *(.sdata .sdata* gnu.linkonce.sd.*) /* секция инициализированных данных "small"*/ . = ALIGN(4); __sdata_end__ = .; } >SRAM AT>FLASH __sdata_load_end__ = __sdata_load_start__ + SIZEOF(.sdata); .sbss (NOLOAD): { __sbss_start__ = __sdata_end__ ; *(.sbss* gnu.linkonce.sb.*) /* секция не ининициализированных данных "small" */ . = ALIGN(4) ; __sbss_end__ = .; } >SRAM
здесь видно что наши маленькие данные будут прижаты к началу SRAM, линкет вычислит центр области и присвот символу
__global_pointer$ = ORIGIN(SRAM) + 2K;
середины области 4K.
Далее при указании линкеру ключика -Wl,--relax-gp и(или) -Wl,--relax происходит некое волшебство:
сначала линкуется бинарь как обысно, выполняется связывание адресов.
в этой точке уже известно что глобальный регистр БУДЕТ содержать в данном случае адресс ORIGIN(SRAM) + 2K;
делается проход оптимизации под названием "релаксация кода", и если до данных из конкретного места программы можно дотянутся через $gp и 12 битное смещение - пара инструкций заменяется на одну
выше описанное и есть содержание вопроса.
Но как всегда ... есть нюанс!
Линкер ПРЕДПОЛАГАЕТ что $gp содержит правильный адрес. А откуда он там возмется при запуске реальной программы?? МЫ ЕГО ТУДА ДОЛЖНЫ ЗАПИСАТЬ! ни компилятор, ни линкер об этом ничего не знают и этого не умеют.
самое удобное место для этого - crt код, начальный участок программы в обработчике сброса:
у меня это выглядит так:
__ais__ void gp(const void* val) { asm volatile ( ".option push; .option norelax; la gp, %[val]; ; .option pop;" : : [val]"i"(val) : ) ; }
void __attribute__((used,naked)) reset_handler() { #ifdef __CRT_IMPL_GDB_DEALY__ // задержка после сброса, для обеспечения захвата отдажчиком GDB nop_while(__CRT_IMPL_GDB_DEALY__); #endif // установка глобального указателя gp( gnu_linker_global_pointer()) ; // установка указателя стека sp( gnu_linker_main_stack_end() ) ; // настройка конвейера процессора corecfg_t::val(0x1f); // установка вложенных прерываний и аппаратного стека intsysc_t::write( intsysc_t::hardware_stack_t::enable, intsysc_t::interrupt_preemption_t::enable, intsysc_t::preemption_t::level4, intsysc_t::interrupt_enable_after_hpe_overflow_t::enable );
.....
в периферийной библиотеке WCH это выглядит так:
.section .handle_reset,"ax",@progbits
.weak handle_reset
.align 1
handle_reset:
.option push
.option norelax
la gp, __global_pointer$
.option pop
la sp, _eusrstack
/* Load loadcode section from flash to RAM */
la a0, _loadcode_lma
la a1, _loadcode_vma_start
la a2, _loadcode_vma_end
bgeu a1, a2, 2f
при компляции объектника компиллер сгенерит код записи в $gp значений глобального символа __global_pointer$, а линкер его вычислит и подствид в код инструкции.
на этом циклическая много ходовка завершается и все работает как часики.
замечания:
если не выполнить код la gp, __global_pointer$, дальнейший код будет кривым и приведет к эксепшену
если независимо, от выполнения кода la gp, __global_pointer$, и содержания __global_pointer$ НЕ ИСПОЛЗОВАТЬ релаксацию ( --relax), оптимизация для использования $gp выполнятся не будет, в таком случае, запись чтение будет выполнятся по стандартной схеме двумя инструкциями.
если Вы не используете релаксация - и Ваш код не использует $gp по назначению, используйте его как обычныйрегистр по своему усмотрению
обычно $gp выставляеься при старте программы и больше не меняется, а код полагает что ему известно его неизменное содержимое, НО ни кто не запрещает Вам на лету полдменять $gp и сразу за этим испольнять куски кода. которые расчитаны на текущее содержимое.
из вышеприведенного заключения проистекает важное следствие - участков "волшебной области памяти ±2048 байт" может быть хоть мильон - главное правилно разжевать это линкеру, и выполнять по ходу программы диспетчерезацию содержания $gp. хоть всю ОЗУ разбить на участки.
если приложить ручки - можно произвести более изящные тюки чем оптимизация кода, поскольку адресация через $gp есть косвенная... значит можно лепить полиморфный код. кто там потом разберется что и откуда исполнялос? после затирания $gp концы вводу.
отключить эту птимизацию сложно. поскольку она относится и к "уменьшению размера" и к "eускорению", соответсвенно входит в множесва оптимизация -02 -O3 -Os -Ofast. по этому на всякий случай всегда в таких случаях нужно проверить дизассемблированый код на использование $gp
если мам попадется не контролируемая вами внешняя библиотека библиотека $gp - она может вам все порушить. нужжно это понимать.
если внимательно посмотреть в мой скрипт линкера - можно заметить что первыми в раскладке идут странные секции .sudata .subss, для компилятора они нестандартны, он туда ничего не положит если его не заставить. Эти секции определяемые нами.
struct user_small_data_t
{
int a ;
int b ;
} ;
__attribute__((section(".sudata"))) user_small_data_t usd ;
такой код сообщит компилятору в какой секции должны лежать наши пользовательские данные - в .sudata, а линкет их там разместит и они окажутся в адресуемом $gp регионе памяти независимо от желания компилятора и их размера.
7) теперь результаты экспериментов/ мой типовой проектик на ch32v303cbt. FreeRTOS, куча, консоль vt100, шим, ацп, i2c oled. добавил в проект вывод в консоль содержания секций для демонстрации
в консоль желтым цветом выведены секции данных которые попадают в область адресуемую через $gp. Задача напихать туда как можно больше
без явного указания -msmall-data-limit=xxx
количестов иструкций в коде с использованием $gp - 611 штук
-msmall-data-limit=32

)
количестов иструкций в коде с использованием $gp - 630 штук
видно чуток пожалось
а поробуем побольше сунуть...
-msmall-data-limit=512

количестов иструкций в коде с использованием $gp - 677 штук
еще код пожался
тут видно что даже .bss частично попала в нашу область - хорошо!
при моих раскладах проектика - увеличение -msmall-data-limit более 512 уже ничего не меняет. значит отставляю 512, оптимизация закончена.
немного подсчетов - мы добились использования 677 раз доступа одной инструцией. это значит сэкономти 677*4 байт кода, и при этом ускорили выполнение по сравнению с кодом который не использует релаксацию.
учитывая что для некоторых микроконтроллров типа ch32v003 ВСЯ ОЗУ влазит в 4К a флэш крошечнй, можно утверждать что описанные выше мероприятия в таких случаях для грамотного разработчика - обязательны, ибо эффект ожидается драмматический
как то так этим можно и нужно управлять.