Многие вещи нам непонятны не потому, что наши понятия слабы; а потому, что эти вещи не входят в круг наших понятий.
Вчера, просматривая новый комментарий к моему старому посту о реализации сдвигов компилятора gcc для МК, я заметил интересную ссылку godbolt.org/z/lW6rk8 .
Он демонстрирует разницу в коде, сгенерированном компилятором для знаковых и беззнаковых кардинальных типов.
Я решил немного поэкспериментировать с данным кодом и обнаружил интересные особенности компилятора, о которых сообщаю сообществу.
Итак, мы видим две функции, которые сравнивают определенное значение x и это же значение, увеличенное на 1, и сообщают, какое из них больше.
Подавляющее большинство читателей уже поняли, в чем дело, но для тех, кто забыл курс введения в программирование, напомню.
В обычной математике сама постановка задачи выглядит странной, если не вкладывать в понятие «больше» смысл, отличный от общепринятого.
Естественно, x+1> x, так как если вычесть x из обеих частей неравенства, то получим тождество 1> 0. При этом мы неявно постулируем, что для любого числа существует число, следующее за ним, и число возможных чисел бесконечно.
Примечание (Pnp): я где-то читал, что один из великих людей (например, Гаусс) считал, что существует наибольшее целое число, но даже если это было правдой, он, вероятно, имел в виду что-то другое.
Но в компьютерной математике не всё так просто и проблема возникает из-за ограниченности кардинальных типов, поскольку это означает конечное число объектов, которые могут быть представлены определённым типом для данной кодировки.
Среди них есть максимальное, в каком-то смысле, число, и попытка его увеличить приводит к парадоксу — нужно закодировать число, которое невозможно закодировать.
Это явление обычно называют переполнением сетки или просто переполнением.
Типичный пример: 255 для 8-битного числа.
Pnp: В детстве одной из моих любимых игрушек была счетная машинка «Феликс», и моим постоянным развлечением было сложение числа «000000001» с «999999999».
Я зачарованно наблюдал, как одна за другой девятки превращались в нули и этот процесс завершился звонком.
Еще более мистическим было вычитание из результата той же самой и девятки чудесным образом появлялись снова.
На другой моей игрушке - счетах (мама была бухгалтером) происходили те же процессы, но там я сам катал домино, а на арифмометре это была какая-то мистика.
Итак, для целых чисел со знаком парадокс решается просто — его появление запрещено, поэтому переполнение приводит к UB. И, поскольку относительно ее результата справедливы любые предположения (в том числе и фантастические), компилятор имеет право предположить, что x+1 всегда будет больше x, и вернуть значение «истина», что он и делает в первой функции.
.
Пнп: В DSP используется другой подход (импортозамещение DSP) — операции с насыщением, при этом x+1 может быть равно x, но это пока экзотика.
А вот для беззнаковых целых чисел результат прибавления 1 к максимальному числу определен и равен нулю.
Следовательно, 0xFFFF+0x0001=0x0000 > 0xFFFF неверно, и компилятор должен сгенерировать код для проверки этого случая, что мы видим во второй функции.
Пока ничего интересного для читателя «по теме» я не написал, но вот тут начинается.
Для изучения ассемблерного кода я перешёл на свои любимые контроллеры AVR (их ассемблер проще и понятнее, чем у ARM).
Для начала давайте посмотрим, как gcc выполняет заказанную нами проверку.
Берём число х в буфер (строки 2,3), прибавляем к нему 1 (4,5), подготавливаем результат (6), сравниваем буфер с числом х (7,8), корректируем результат (9 ,10) и задача выполнена.alwaysTrue_unsigned(unsigned int): mov r18,r24 mov r19,r25 subi r18,lo8(-(1)) sbci r19,hi8(-(1)) ldi r20,lo8(1) cp r24,r18 cpc r25,r19 brlo .
L3 ldi r20,lo8(0) .
L3: mov r24,r20 ret
Всего 9 команд и время выполнения 8+ тактов.
Но это версия 5.4.0.
А вот вывод версии 9.2.0: alwaysTrue_unsigned(unsigned int):
mov r19,r25
mov r18,r24
ldi r24,lo8(1)
cpi r18,-1
sbci r19,-1
breq .
L5 ret .
L5:
ldi r24,0
ret
и мы видим, что компилятор реализует совсем другую проверку.
Берём число x в буфер (2,3), подготавливаем результат (4), вычитаем из буфера 0xFFFF (5,6) и корректируем результат. Всего 7 команд и время выполнения 6+ тактов.
Если мы переведем этот код обратно на C, мы получим что-то вроде: return (x!=0xFF)
И такое преобразование абсолютно допустимо, но я не имею ни малейшего представления о том, как компилятор делает такие преобразования, это просто сказка какая-то.
Также обратите внимание на код проверки выражения (x+4)> x и так далее.
Потом я решил поиграться с типами и попробовал (u)int16_t — ничего не изменилось, (u)int32_t — появились загадочные передачи, но смысл проверки остался прежним, я использовал (u)int8_t и обалдел.
Совершенно неожиданно код беззнакового аргумента сократился и функция стала выдавать жёсткий.
Но это откровенно неверно и для случая x=0xFF условие (x+1)> x не будет выполнено.
Ура, я действительно нашел ошибку в gcc (в плане оптимизации) и могу сообщить о ней и помочь в разработке такого мощного продукта.
Пнп: Когда лет 15 назад мои мальчики пришли с фразой «Папа, я нашел ошибку в компиляторе» (мы говорили о ТурбоПаскале), я отправил их с предложением повнимательнее посмотреть на их код и ошибку компилятора.
разрешилась сама собой, но это совсем другое дело — я не могу допустить неточности в трактовке предельно ясного случая.
Но счастье было недолгим.
На самом деле, нужно внимательно прочитать стандарт языка Си и завеса тайны приоткроется (я этого не делал и просто догадался).
Вы тоже можете это догадаться Выражение (x+1) в операторе сравнения имеет тип int (в данном случае uint16_t), поэтому перед выполнением сложения тип uint8_t преобразуется в тип uint16_t путем присвоения нулевого старшего байта, после чего переполнение становится невозможным.
и результат функции предрешен.
Достаточно просто явно обозначить свои намерения сравнивать байты следующим образом: Return (uint8_t)(x+1)>x
и необходимая проверка возвращается в ассемблерный код.
Вариант return (x+(unsigned char)1) > x;
не катит, что характерно.
Наверняка существует прагма компилятора, которая по умолчанию сделает int 16-битным и позволит вернуть проверку значения x, но это немного сложнее.
Вот забавная предновогодняя история, которая лично мне дала больше понимания преобразования кардинальных типов в C (теперь я ее никогда не забуду), чем возможное чтение стандартов (но это вовсе не значит, что нельзя читать их).
Теги: #Программирование микроконтроллеров #программирование #C++
-
Советы По Покупке Нового Компьютера
19 Oct, 24 -
Tortoisesvn В Ubuntu Быть!
19 Oct, 24 -
Мы Оценим Вашу Деятельность. Недорогой
19 Oct, 24 -
Рабдологические Счеты Клода Перро
19 Oct, 24 -
Топ-6 Оптимизаций Для Netty
19 Oct, 24