Перейти к содержанию
    

Переупорядочивание инструкций

51 минуту назад, AlexRayne сказал:

...если функция не static, по прямо таки интересно - когда он её заинлайнит?

Да вот возьмите хоть тот же ARM Compiler 6.
Его тоже на оптимизации выше -O1 сложно заставить не инлайнить короткие функции.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Меня больше всего удивило когда я однажды изобразил умножение на 10 с помощью сдвигов и сложений, а gcc заменил это на одну инструкцию mul, или даже деление там было.

 

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

Изменено пользователем amaora

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

3 часа назад, AlexRayne сказал:

а какая тут может быть проблема?

Например: Есть функция f1() из которой вызываются f2() и f3(). Если f2, f3() не инлайнятся, то их локальные переменные занимают одно и то же место на стеке, если инлайнятся - для каждой выделяется свой отдельный объём на стеке f1(). И если объём локальных переменных f2(), f3() достаточно велик, то может произойти переполнение стека.

Конечно компилятор мог бы объединить массивы локальных переменных f2() и f3() в виде union, но IAR почему-то так не делает и создаёт отдельные наборы переменных.  :sad:

Впрочем - возможно другие компиляторы поступают по иному.....

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

9 часов назад, jcxz сказал:

И если объём локальных переменных f2(), f3() достаточно велик, то может произойти переполнение стека.

Я столкнулся с проблемой на стеке у ГЦЦ при слиянии функций. но у меня проблема лежала в диких case/switch почемуто гну не смог адекватно тогда это переварить, поехала раскладка аргументов ко вложенной функции на стеке.

но это эксклюзив же. если функция большая и сложная - дублировать её тело компилятор не станет. даже ценой небольшого ускорения - на АРМах цена вызова невелика, а кеш на всех системах пенальти чувствует.

проблема может быть если он массово инлайнит маленькие функции. но тут сложно приписать криминал - как правило это именно то что ожидается от компилера.

10 часов назад, jcxz сказал:

но IAR почему-то так не делает и создаёт отдельные наборы переменных

однако странно что ИАР не оптимизирует стек. у него же спец-инструмент вроде даже есть для оценки расхода стека.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

49 минут назад, AlexRayne сказал:

но это эксклюзив же. если функция большая и сложная - дублировать её тело компилятор не станет.

Для того чтобы использовать много стека, функции не обязательно быть большой.

49 минут назад, AlexRayne сказал:

однако странно что ИАР не оптимизирует стек. у него же спец-инструмент вроде даже есть для оценки расхода стека.

Нет никакого инструмента. В проекте используется РТОС.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Приветствую еще раз всех.

Есть следующий код (функция чтения данных из программно-аппаратного FIFO UART)

class FIFO {
  ...
  volatile u32  rpos, wpos;               // read and write position
           u8   buf[FIFO_RXBUF_SIZE];     // working data buffer
  volatile bool isOvf, isTrash;           // "overflow" and "there is trash in the data buffer" flags
};

s32 FIFO::read(u8 dst[], u32 len) {
  const u32 bufSize = FIFO_RXBUF_SIZE,
            crpos   = rpos;               // current read position
        u32 bslen   = wpos - crpos;       // busy space length definition
  if((s32)bslen < 0) bslen += bufSize;
  if(len > bslen)    len    = bslen;
  if(len > 0) {
    u32 nrpos = crpos + len;              // next read position
    if(nrpos >= bufSize)
      nrpos -= bufSize;
    if(dst != NULL) {                     // copy data from buffer to the destination
      if(nrpos > crpos)
        memcpy(dst, buf + crpos, len);
      else {
        const u32 llen = nrpos,
                  rlen = bufSize - crpos;
        memcpy(dst, buf + crpos, rlen);
        if(llen > 0)
          memcpy(dst + rlen, buf, llen);
      }
    }
    rpos = nrpos;                         // update read position
    return len;
  }
  else return isOvf || isTrash ? -1 : 0;
}


Функция read() возвращает запрошенное количество данных (с учетом заполненности буфера). Если данных в буфере нету - то либо оповещаем вызывающий код об ошибке (если было переполнение или в буфер прилетел "битый" с точки зрения периферии UART кадр-символ), либо говорим, что данных нет.

Потенциальная проблема в следующем. Строка обновления rpos = nrpos должна быть вызвана именно в том месте, где она расположена в коде. Это должен гарантировать компилятор. Технически между копированием данных из буфера в пользовательский dst[] (строки с memcpy()) и rpos = nrpos нет ни зависимости по данным, ни по побочным эффектам. Соответственно, оптимизатор вполне может переместить rpos = nrpos куда-то выше копирований. Не зря же пишут в описании __schedule_barrier() о том, что обычные точки следования допускают перемежение операций с побочными эффектами, которые не влияют друг на друга.

Должен ли я намеренно поставить барьер оптимизации перед rpos = nrpos, или же я сейчас упускаю какой-то момент? В листинге все чисто на всех уровнях оптимизации - но гарантированно ли это? Просто так воткнуть DMB() не катит - можно, но это не добавит мне понимания и академически-практических скилов.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Я в таком месте ставлю барьер, а квалификатор volatile не использую. Можно через atomic обёртку обращаться к указателям чтения/записи, здесь это будет уместно.

 

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

 

Инструкция упорядочивания записи для процессора соответственно тоже нужна, если такая имеется. Вставка DMB обычно уже сделана так, что содержит и компиляторный барьер и инструкцию.

 

На псевдокоде:

_rpos = atomic_get(&rpos);
_wpos = atomic_get(&wpos);

if (_rpos != _wpos) {
  
  _nrpos = _rpos + 1;

  memcpy(dst, &queue[rpos], ...);

  __compiler_memory_fence();
  __memory_barrier_instruction();

  atomic_set(&rpos, _nrpos);
}

 

Добавка: атомики здесь для того чтобы подчеркнуть намерения, что эти значения никак нельзя читать/писать по частям (побайтно или как-то ещё).

Изменено пользователем amaora

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Просматривал дизасм для следующей функции

static struct {
  u32  volatile rpos, wpos;
  u8            buf[FIFO_TX_SIZE];
  
  bool volatile isStart;
  u32  volatile reqSize;
} TxFIFO;


s32 writeTxFIFO(u8 src[], u32 len) {
  if(len > 0) {
    __disable_irq();
    u32 const curRPos    = TxFIFO.rpos,
              curReqSize = TxFIFO.reqSize,
              curReqNDTR = DMA1_Channel4->CNDTR;
    __enable_irq();
    
    u32 const bufSize = FIFO_TX_SIZE;
    u32       nxtRPos = curRPos + curReqSize - curReqNDTR;
    
    if(nxtRPos >= bufSize)
      nxtRPos -= bufSize;
    
    u32 const curWPos = TxFIFO.wpos;
    u32       freeLen = nxtRPos;
    
    if((s32)(freeLen -= curWPos) <= 0)
      freeLen += bufSize;
    
    if(freeLen - 1 >= len) {
      u32 llen    = 0, rlen = len,
          nxtWPos = curWPos + len;
      
      if(nxtWPos >= bufSize)
        nxtWPos -= bufSize,
        llen     = nxtWPos,
        rlen    -= llen;
      
      memcpy(&TxFIFO.buf[curWPos], src, rlen);
      
      if(llen > 0)
        memcpy(TxFIFO.buf, &src[rlen], llen);
      
      TxFIFO.wpos = nxtWPos;
      
      if(!TxFIFO.isStart)
        TxFIFO.isStart = true,
        reqTxDMA(&TxFIFO.buf[curWPos], rlen);
    }
    else return -1;
  }
  
  return 0;
}


Функция записывает в программный FIFO и запускает DMA-транзакцию, если последнее необходимо. Не суть.

В начале небольшая критическая секция вычитывает volatile-переменные из памяти, чтобы дальше можно было оптимизировать.

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

_ZN6nsUART11writeTxFIFOEPhj
        0x08002670:    b5f0        PUSH     {r4-r7,lr}
        0x08002672:    b083        SUB      sp,sp,#0xc
        0x08002674:    2800        CMP      r0,#0
        0x08002676:    d04f        BEQ      0x8002718 ; _ZN6nsUART11writeTxFIFOEPhj + 168
        0x08002678:    4604        MOV      r4,r0
        0x0800267a:    f3ef8010    MRS      r0,PRIMASK
        0x0800267e:    b672        CPSID    i
        0x08002680:    4b26        LDR      r3,[pc,#152] ; [0x800271c] = 0x200002cc
        0x08002682:    6818        LDR      r0,[r3,#0]
        0x08002684:    2143        MOVS     r1,#0x43
        0x08002686:    008e        LSLS     r6,r1,#2
        0x08002688:    5999        LDR      r1,[r3,r6]
        0x0800268a:    1808        ADDS     r0,r1,r0
        0x0800268c:    4924        LDR      r1,[pc,#144] ; [0x8002720] = 0x40020044
        0x0800268e:    6849        LDR      r1,[r1,#4]
        0x08002690:    1a42        SUBS     r2,r0,r1
        0x08002692:    20ff        MOVS     r0,#0xff
        0x08002694:    43c0        MVNS     r0,r0
        0x08002696:    2aff        CMP      r2,#0xff
        0x08002698:    d900        BLS      0x800269c ; _ZN6nsUART11writeTxFIFOEPhj + 44
        0x0800269a:    1812        ADDS     r2,r2,r0
        0x0800269c:    b662        CPSIE    i
        0x0800269e:    6859        LDR      r1,[r3,#4]
        0x080026a0:    1a52        SUBS     r2,r2,r1
        
        ...


Есть ли способ (кроме всяких пюре-асм) сказать компилятору, что между точками А и Б в коде нужно проводить только операции загрузки из памяти? Т.е. я хочу, чтобы до критической секции в регистрах уже лежали все нужные адреса, смещения и т.д., а внутри критической секции было 3 инструкции LDR? А то он накомпилировал аж проверку if(nxtRPos >= bufSize) nxtRPos -= bufSize; внутри критической секции, а оно мне тут даром не надо:sad:

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Барьерчики попробуйте. На выбор:

/**
  \brief   Instruction Synchronization Barrier
  \details Instruction Synchronization Barrier flushes the pipeline in the processor,
           so that all instructions following the ISB are fetched from cache or memory,
           after the instruction has been completed.
 */
__attribute__((always_inline)) __STATIC_INLINE void __ISB(void)
{
  __ASM volatile ("isb 0xF":::"memory");
}


/**
  \brief   Data Synchronization Barrier
  \details Acts as a special kind of Data Memory Barrier.
           It completes when all explicit memory accesses before this instruction complete.
 */
__attribute__((always_inline)) __STATIC_INLINE void __DSB(void)
{
  __ASM volatile ("dsb 0xF":::"memory");
}


/**
  \brief   Data Memory Barrier
  \details Ensures the apparent order of the explicit memory operations before
           and after the instruction, without ensuring their completion.
 */
__attribute__((always_inline)) __STATIC_INLINE void __DMB(void)
{
  __ASM volatile ("dmb 0xF":::"memory");
}

Или можно без всяких барьеров, просто

asm volatile ("":::"memory");

(тут я не уверен)

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Нет, ничего из этого не помогает(

Хотел еще попробовать изобразить как-то предварительное формирование адресов volatile-переменных, а их чтение организовать уже в критической секции (чтобы вынести из крит. секции суп из адресов, сдвигов и т.д.)

u32 volatile * volatile i = &TxFIFO.rpos,
             * volatile j = &TxFIFO.reqSize,
             * volatile k = &DMA1_Channel4->CNDTR;
__disable_irq();
u32 const curRPos    = *i,
          curReqSize = *j,
          curReqNDTR = *k;
__enable_irq();


Указатели i, j, k пометил как volatile для того, чтобы компилятор насоздавал точки следования и четко высчитал адреса строго до начала критической секции. Однако тут другая проблема - никак не сказать компилятору, чтобы он эти i, j, k хранил в регистрах CPU, а не на стеке функции.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Keil, ARM Compiler 6.

Тут не то что своевольничество (ведь все весьма даже соответствует поведению абстрактной машины Си/++), просто знать бы, мб у компилятора есть средства сказать ему какими-то ключевыми словами "делай что говорят".

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Барьеры для того и придуманы, чтобы говорить компилятору "делай что говорят" 🙂
Вот это : :"memory"  в конце asm-вставки говорит компилятору, что вставка "портит" память, и поэтому компилятор не должен делать предположений о её сохранности после вставки. То есть, по идее как раз то, что нужно.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Вот что у меня gcc выдал:

08003550 <writeTxFIFO(unsigned char*, unsigned long)>:
 8003550:	b5f8      	push	{r3, r4, r5, r6, r7, lr}
 8003552:	0005      	movs	r5, r0
 8003554:	2900      	cmp	r1, #0
 8003556:	d021      	beq.n	800359c <writeTxFIFO(unsigned char*, unsigned long)+0x4c>
 8003558:	b672      	cpsid	i
 800355a:	4c1b      	ldr	r4, [pc, #108]	; (80035c8 <writeTxFIFO(unsigned char*, unsigned long)+0x78>)
 800355c:	4a1b      	ldr	r2, [pc, #108]	; (80035cc <writeTxFIFO(unsigned char*, unsigned long)+0x7c>)
 800355e:	6823      	ldr	r3, [r4, #0]
 8003560:	6960      	ldr	r0, [r4, #20]
 8003562:	6852      	ldr	r2, [r2, #4]
 8003564:	b662      	cpsie	i
 8003566:	181b      	adds	r3, r3, r0
 8003568:	1a9b      	subs	r3, r3, r2
 800356a:	2b07      	cmp	r3, #7
 800356c:	d818      	bhi.n	80035a0 <writeTxFIFO(unsigned char*, unsigned long)+0x50>
...

 

Вот как выглядят

__attribute__( ( always_inline ) ) __STATIC_INLINE void __enable_irq(void)
{
  __ASM volatile ("cpsie i" : : : "memory");
}


__attribute__( ( always_inline ) ) __STATIC_INLINE void __disable_irq(void)
{
  __ASM volatile ("cpsid i" : : : "memory");
}

Всё как и должно быть, клоббер "memory" отрабатывает.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.

Гость
Ответить в этой теме...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...