32bit_me (32bit_me) wrote,
32bit_me
32bit_me

Category:

О коммутативности сложения

Меняется ли ассеблерный код, если вы напишете не (a + b), а (b + a)?

Давайте проверим.

Напишем такой код:

__int128 add1(__int128 a, __int128 b) {
    return b + a;
}


и скомпилируем:

add1(__int128, __int128):
.LFB0:
  .cfi_startproc
  add a0,a2,a0
  sltu a2,a0,a2
  add a1,a3,a1
  add a1,a2,a1
  ret


А теперь такой код:

__int128 add1(__int128 a, __int128 b) {
    return a + b;
}


Получаем:

add1(__int128, __int128):
.LFB0:
  .cfi_startproc
  mv a5,a0
  add a0,a0,a2
  sltu a5,a0,a5
  add a1,a1,a3
  add a1,a5,a1
  ret


Как говорится, найди отличия.

Это был risc-v gcc 8.2.0

Теперь clang (rv64gc trunk). В обоих случаях получаем одинаковый результат:

add1(__int128, __int128): # @add1(__int128, __int128)
  add a1, a1, a3
  add a0, a0, a2
  sltu a2, a0, a2
  add a1, a1, a2
  ret


Результат похож на то, что сгенерировал gcc в первом случае. Вывод: компиляторы сейчас умные, но не все и не всегда.

Давайте попробуем понять, что здесь происходит и почему. Аргументы функции __int128 add1(__int128 a, __int128 b) передаются в регистрах a0, a1, a2, a3, в следующей последовательности: a0 - младшее слово операнда a, a1 - старшее слово операнда a, a2 - младшее слово операнда b, a3 - старшее слово операнда b. Возврат результата - в той же последовательности: a0 - младшее слово, a1 - старшее слово.

Далее мы складываем старшие слова операндов и помещаем результат в a1, и младшие слова, помещая результат в a0. Далее мы сравниваем результат с a2, т.е. с младшим словом операнда b. Это делается для того, чтобы выяснить, был ли перенос при сложении. Если перенос имел место, то результат сложения будет меньше любого операнда. Так как операнда в a0 уже нет, мы используем для сравнения a2. Если a0 < a2, то перенос был, и в a2 записывается 1, иначе 0. Далее прибавляем перенос к старшему слову результата и получаем окончательный результат в (a1, a0).

Здесь можно увидеть небольшую возможность для оптимизации микроархитектуры ядра. Согласно стандарту, микроархитектура может (необязательно) детектировать пары команд (MULH[[S]U] rdh, rs1, rs2; MUL rdl, rs1, rs2) и (DIV[U] rdq, rs1, rs2; REM[U] rdr, rs1, rs2), и обрабатывать эти пары как одну команду. Аналогично можно детектировать пару (add rdl, rs1, rs2; sltu rdh, rdl, rs1/rs2) и сразу производить запись разряда переноса в rdh.

Полностью аналогичный код формируется clang-ом (rv32gc trunk) для 32-битного ядра, если в функции используются 64-битные аргументы и результат:

long long add1(long long a, long long b) {
    return a + b;
}


Ассемблер:

add1(long long, long long): # @add1(long long, long long)
  add a1, a1, a3
  add a0, a0, a2
  sltu a2, a0, a2
  add a1, a1, a2
  ret


Всё абсолютно аналогично. К сожалению, компилятор для 32-битного ядра не поддерживает типа __int128, а жаль.

Но это ещё не вся история...
Tags: C и C++, llvm, risc-v, программирование
Subscribe

  • Сид Мид - дизайнер киберпанка

  • For All Mankind

    For All Mankind - сериал в жанре альтернативной истории. По сюжету сериала, СССР первым осуществил посадку на луну, что привело американского…

  • May, 4th

    May the 4th - день Звёздных Войн! May the Force be with you!

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 9 comments