Переход от кода C к коду сборки RISC-V

#c #assembly #riscv

Вопрос:

У меня есть это упражнение

Переведите следующий код C в код сборки RISC-V. Предположим, что значения a, b, i и j находятся в регистрах x5, x6, x7 и x29 соответственно. Кроме того, предположим, что регистр x10 содержит базовый адрес массива D.

 for(i=0; i<a; i  ){
    for(j=0; j<b; j  ){
        D[4*j] = i   j;
    }
}
 

У меня также есть решение с комментариями

 Loop1:
    addi x7, x0, 0      // i = 0
    bge  x7, x5, ENDi   // while i < a
    addi x30, x10, 0    // x30 = amp;D[0]
    addi x29, x0, 0     // j = 0
Loop2:
    bge x29, x6, ENDj   // while j < b
    add x31, x7, x29    // x31 = i   j
    sd  x31, 0(x30)     // D[4*j] = x31
    addi x30, x30, 32   // x30 = amp;D[4*(j 1)]
    addi x29, x29, 1    // j  
    jal  x0,  LOOP2
ENDj:
    addi x7, x7, 1      // i  
    jal  x0, LOOP1
ENDi:
 

Чего я не понимаю

 sd  x31, 0(x30)     // D[4*j] = x31
addi x30, x30, 32   // x30 = amp;D[4*(j 1)]
 

Не sd x31, 0(x30) означает ли это, что я сохраняю значение x31 в 0-м бите массива 30? Откуда вдруг взялся 4*j?

И не addi x30, x30, 32 означает ли это, что x30 = x30 32? И разве x30 = amp;D[0] не было определено в первом цикле? Как j внезапно вступает в контакт с x30?

Ответ №1:

Это эквивалентно следующему:

 for(int64_t i=0; i<a; i  ){
    int64_t* x30 = amp;D[0];
    for(int64_t j=0; j<b; j  ){
        *x30 = i   j;
        x30  = 4; // increment by 4 elements
    }
}
 

x30 используется в качестве «текущего указателя» в цикле. Он указывает на соответствующий элемент в массиве D. Он инициализируется первым элементом и увеличивается на 4 шага по 4 элемента, что эмулирует часть 4*j. Поскольку ваши элементы 64-разрядные, 8 байт, для увеличения на 4 элемента базовый адрес указателя должен быть увеличен на 4*8=32 байта.

Часто более эффективно поддерживать текущий указатель, чем каждый раз его пересчитывать, особенно в RISC-V, потому что у вас не может быть смещения регистра (его нет str x31, [x30, x29] ).

Комментарии:

1. Спасибо за ответ, но я все еще в замешательстве. Можете ли вы уточнить, что делает этот код sd x31, 0(x30) ? Потому что я не вижу связи между кодом и комментарием в решении.

2. @Carl sd x31, 0(x30) буквально хранит x31 по адресу в x30, поэтому *x30 = x31 в C сохраняйте элемент, на который указывает x30, что эквивалентно x30[0] = x31 (* имеет больше смысла для указателей и [] для массивов).

3. @Carl x30 увеличивается одновременно с j, чтобы прогрессировать в массиве. При использовании j доступы будут D[0] , D[4] , D[8] … Индекс к D увеличивается на 4 элемента в каждом цикле, поэтому наличие x30 начинается с amp;D, и увеличение на 4 элемента в каждом цикле эквивалентно. В основном, это делание D[0]; D =4 , D[0]; D =4 , D[0]; D =4 . При увеличении x30 , которое первоначально указывало на D[0], вы указываете на следующие элементы, поэтому x30 = amp;D[0] со x30 = 4 средствами x30 указывает на D[4].

4. @Carl, если D-массив , который вы на самом деле не можете сделать D =4 , но в C (и сборке) почти нет разницы между массивами и указателями. Если вместо этого вы используете указатель, как в int64_t* x30 = amp;D[0]; , то x30 указывает на D[0], и выполнение x30 = 4; каждого цикла позволит вам получить доступ D[0] , D[4] , D[8] … Каждый элемент массива является последовательным в памяти. Таким образом, если вы возьмете адрес/указатель на первый элемент, а затем увеличите его, это позволит вам получить доступ к следующим элементам массива.

5. @Carl: Оптимизация D[j*4] в ptr =4 связана с «уменьшением прочности». en.wikipedia.org/wiki/Strength_reduction . Вы заменяете вычисление генерации сдвига / добавления адреса в цикле только приращением указателя. Это еще более важно, если бы множитель не был простой степенью 2 (потому что сдвиги все еще относительно дешевы, т. Е. Не настолько «сильны»), но это все еще случай сокращения нескольких операций до одной.