Давайте проверим.
Напишем такой код:
__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, а жаль.
Но это ещё не вся история...