LINUX.ORG.RU

Авторы Си — наркоманы?

 , , ,


1

5

Столкнулся с интересным багом. После того как разобрался, что же именно происходит, меня постигло крайнее изумление! Оказывается, в языке Си тип числовой константы зависит от формата записи.

Дистиллированный пример кода, который это демонстрирует:

#include <stdbool.h>
#include <stdio.h>

#define IS_HEX(x) \
    _Generic((x), \
        unsigned int: true, \
        long: false \
    )

#define X 0x80000001
#define I 2147483649

int main(void) {
    if(X == I)
        puts("X == I");

    if(!IS_HEX(I))
        puts("I is not hexadecimal");

    if(IS_HEX(X))
        puts("X is hexadecimal");

    return 0;
}

Все три сообщения будут выведены на экран.

Зачем это сделано? Кому от этого легче? Какие оптимизации это позволяет проворачивать, кроме оптимизации отстрела ног программистам? Непонятно! В общем, стремлюсь поделиться своим негодованием здесь и предостеречь будущие поколения от наступления на эти грабли.



Последнее исправление: dataman (всего исправлений: 1)
Ответ на: комментарий от algo

Ну да, я помню

Это цикл:

DO 3 I = 1,3

А это присвоение переменной:

DO 3 I = 1.3

Говорят, из-за этой разницы Маринер-1 на Венеру не попал.

monk ★★★★★
()
Ответ на: комментарий от firkax

Си на фоне тех языков даже неплохо смотрится.

monk ★★★★★
()
Ответ на: комментарий от yorshka

Чувак, там список 13 страниц формата A4. У тебя точно он голове помещается? Весь? Ты точно в этом уверен?

Ну да :)

Наа практике такого не бывает, что опытный разработчик напоролся на UB, про которое он не знал, что это UB. Ну по крайней мере на сишке. В C++ может и бывает. (На расте точно бывает, благодаря средней компетенции растаманов — см. хотя бы следующий комментарий.)

Прямо как в Си! Свежие стандарты добавляют UB только в путь. В C17 вызов realloc(ptr, 0) специально сделали определённым поведением, а потом в C23 сделали неопределённым. Видишь как круто сишники умеют?

Так программа пишется под конкретный стандарт, а не под «самый новый си». И этот стандарт фиксируется с помощью -std=c99.

А по поводу realloc — они просто узаконили беззаконие, которое и так было и на которое нельзя было полагаться при написании портабельных программ: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf

shdown ★★
()
Последнее исправление: shdown (всего исправлений: 1)
Ответ на: комментарий от unC0Rr

Так это и не уб. УБ это будет, когда ты разыменуешь эту ссылку.

https://doc.rust-lang.org/reference/behavior-considered-undefined.html#r-unde...

A reference or Box<T> must be aligned and non-null, it cannot be dangling, and it must point to a valid value (in case of dynamically sized types, using the actual dynamic type of the pointee as determined by the metadata). Note that the last point (about pointing to a valid value) remains a subject of some debate.

Азаза, поймали растамана на незнании UB раста :)))))

shdown ★★
()
Ответ на: комментарий от zurg

Лол чего, это каких? Сильно меньше чем в сишке, и как раз очевиднее.

Нет, конечно:

Модель есть, но нельзя однозначно и гарантированно выявить, независимо от языка, в чём и злокозненость понятия UB

Это ложь.

https://doc.rust-lang.org/reference/behavior-considered-undefined.html

There is no formal model of Rust’s semantics for what is and is not allowed in unsafe code, so there may be more behavior considered unsafe.

shdown ★★
()
Ответ на: комментарий от shdown

Ну попробуй не разыменовать, а хотя бы распечатать адрес этой ссылки dbg!(dangling as *const i32 as usize);. Неиспользуемая переменная всё равно что не существующая.

unC0Rr ★★★★★
()
Ответ на: комментарий от shdown

На практике такого не бывает, что опытный разработчик напоролся на UB, про которое он не знал, что это UB. Ну по крайней мере на сишке.

В openssl неопытные разработчики?

monk ★★★★★
()
Ответ на: комментарий от monk

Все опытные разработчики сидят в тредах на лоре и свой код не показывают. Опенссл и другие проекты пишут сплошь нубы, ты разве не знал?

unC0Rr ★★★★★
()
Ответ на: комментарий от monk

В нормальных компиляторах проверяется много что. Проблема в том что были те которые это позволяли.

Slackware_user ★★★★★
()
Ответ на: комментарий от unC0Rr

Попробовал. И в обычном Rust, и через MIRI выполняется без проблем. И даже с println!("{}", dangling);. Так зачем мне что-то было пробовать?

Это UB (даже без использования)? UB. MIRI не ловит? Не ловит. What’s your point?

shdown ★★
()
Ответ на: комментарий от shdown

Наа практике такого не бывает, что опытный разработчик напоролся на UB, про которое он не знал, что это UB. Ну по крайней мере на сишке.

Это, конечно, наглая ложь. В ядре твоё UB находят с завидной регулярностью. В основном из-за рефакторинга, когда поменяли один кусок кода, а в другой, связанный с первым, проверить забыли.

Так программа пишется под конкретный стандарт, а не под «самый новый си». И этот стандарт фиксируется с помощью -std=c99.

Круто. А что делать, если хочешь новый стандарт? Перелопачивать весь код?

которое и так было и на которое нельзя было полагаться при написании портабельных программ

Беззакония не было. Было «implementation-defined behaviour». Они убрали все гарантии, и теперь компилятор может, например, выкинуть всю ветку с realloc(ptr, 0) нахрен, ведь UB в корректном коде быть не может. Или будет считать, что переменная с размером никогда не будет 0, и выкинет какую-нибудь другую ветку. Сишечка славна подобными оптимизациями, стреляющими сишникам в член.

yorshka
() автор топика
Ответ на: комментарий от monk

The Debian maintainer asked for help with code he didn't understand, but the snippets in his post to the OpenSSL list don't include enough context to understand where the MD_update calls really are in the code

The OpenSSL developers responded incorrectly, probably without actually looking at the code to see which calls were being talked about

Ага. А где опытный разработчик напоролся на UB, про которое он не знал, что это UB? Авторы OpenSSL знали, что чтение неинициализированной памяти — это UB, но данный случай ни один компилятор бы не оптимизировал, потому что он не может предсказать, что не вычитаем из файла все 100 байт.

shdown ★★
()
Ответ на: комментарий от yorshka

Это, конечно, наглая ложь. В ядре твоё UB находят с завидной регулярностью. В основном из-за рефакторинга, когда поменяли один кусок кода, а в другой, связанный с первым, проверить забыли.

Ага. А где опытный разработчик напоролся на UB, про которое он не знал, что это UB? Т.е. вот написал, например,

size_t a = something();
// В 'hi' будут верхние 32 бита, в 'lo' — нижние, и ниипёт
uint32_t hi = a >> 32, lo = a;
?

Круто. А что делать, если хочешь новый стандарт? Перелопачивать весь код?

Ну конечно. Они где-то заявляют обратную совместимость? Насколько я знаю, нет.

Беззакония не было. Было «implementation-defined behaviour».

Было. Посмотри таблицу, оно не укладывалось в то, что стандарт называет implementation-defined behavior.

shdown ★★
()
Последнее исправление: shdown (всего исправлений: 1)
Ответ на: комментарий от shdown

Ну вообще, особо вредительский компилятор мог бы подменить RAND_add(buf,n,i) на RAND_add(buf,n,n) т.к. решил бы что n==i. Но такими компиляторами не надо пользоваться.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Не мог, RAND_add находится в другом translation unit. Компилятор не может знать, что оно читает до размера во втором аргументе.

shdown ★★
()
Последнее исправление: shdown (всего исправлений: 1)
Ответ на: комментарий от Slackware_user

int платформозависимый, тут либо алиас на 32 либо на 64-битный. Вроде на 32.

Так я про это же. Либо тебе надо писать отдельную программу под каждую платформу, либо не использовать типы с фиксированной размерностью.

monk ★★★★★
()
Ответ на: комментарий от shdown

А где опытный разработчик напоролся на UB, про которое он не знал, что это UB?

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

yorshka
() автор топика
Ответ на: комментарий от unC0Rr

Ну покажи команду, как ты это запускал. У меня не воспроизводится. И попробуй без какого-либо использования — это ж тоже UB всё равно.

shdown ★★
()
Ответ на: комментарий от shdown

Ну конечно. Они где-то заявляют обратную совместимость? Насколько я знаю, нет.

P.S. В Rust тоже ломают обратную совместимость часто. Но там, справедливости ради, есть Releases. «Круто. А что делать, если хочешь новые фичи? Перелопачивать весь код?»

shdown ★★
()
Ответ на: комментарий от shdown

Инициативные оптимизаторы могли включить lto, как уже вроде где-то случалось с explicit_bzero() или чем-то похожим.

firkax ★★★★★
()
Ответ на: комментарий от shdown

Назначить имя какой-то value не UB, а в момент формирования самой ссылки она ещё валидна. Запуск MIRI через кнопку TOOLS там вверху справа.

unC0Rr ★★★★★
()
Ответ на: комментарий от shdown

Разбей на крейты старый и новый код, каждому свой едишн присобачь. Всё лучше, чем мешать инклуды разных версий цпп.

unC0Rr ★★★★★
()
Ответ на: комментарий от shdown

но данный случай ни один компилятор бы не оптимизировал

Интересная аргументация.

Ладно, как опытный разработчик найдёшь UB в этом коде:

time_t integral_value_holding_the_number_of_seconds_since_the_beginning_of_epoch_without_leap_seconds = time(NULL);
time_t integral_value_holding_the_number_of_seconds_since_the_beginning_of_epoch = integral_value_holding_the_number_of_seconds_since_the_beginning_of_epoch_without_leap_seconds + leap_seconds;

?

monk ★★★★★
()
Ответ на: комментарий от Slackware_user

либо в подобных случаях кастовать в 64 когда надо.

В смысле, вместо int везде int64 делать?

А потом очередная функция хочет *int и туда указатель на int64 кидать?

Менее 32 вроде только в win16 типы были.

На x86 мир не заканчивается

monk ★★★★★
()
Ответ на: комментарий от monk

1. На древнем компиляторе может быть лимит significant characters.

2. Неизвестно, что такое leap_seconds, какого она типа, инициализирована ли, какое значение содержит. time_t может быть знаковым, тогда там может произойти переполнение или недополнение (если time_t = int, time_t leap_seconds = INT_MIN, time(NULL) вернул -1, потому что сейчас 2039-ый год). Если оно не time_t, то уже другой разговор и причин для UB там может быть много.

shdown ★★
()
Последнее исправление: shdown (всего исправлений: 2)
Ответ на: комментарий от unC0Rr

Окей, засчитано, у меня на локалхосте не воспризводится.

Назначить имя какой-то value не UB, а в момент формирования самой ссылки она ещё валидна

Очень сомнительно. А где у авторитетных растаманов это можно уточнить?

shdown ★★
()
Ответ на: комментарий от shdown

Да хз. Вон, есть официальный метод для создания висячих указателей, даже ансейфа не надо: https://doc.rust-lang.org/beta/std/ptr/struct.NonNull.html#method.dangling ну или если прямо сырой надо: https://doc.rust-lang.org/std/ptr/fn.dangling.html

unC0Rr ★★★★★
()
Последнее исправление: unC0Rr (всего исправлений: 1)
Ответ на: комментарий от shdown

Ладно, верю. Я неопытный разработчик: п.1 не глядя в Стандарт не увидел бы.

monk ★★★★★
()
Ответ на: комментарий от zurg

учитывая возраст языков rust не доживёт до почётного(хотя бы из за времени) Сяшки

ибо Си это зерно гениев + бизнесу через ~10 лет стало интересно и окомитетили выгнав плюсы в отдельную эклезию

а rust хуже появился чем даже железная(из франции а не острова) ADA

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

qulinxao3 ★☆
()
Последнее исправление: qulinxao3 (всего исправлений: 1)
Ответ на: комментарий от yorshka

стандарт ближе к религиозным текстам по содержимому чем к собственно формальным документам, и интерпретировать его стоит аналогично

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

vM ★★★
()
Ответ на: комментарий от Slackware_user

Менее 32 вроде только в win16 типы были.

На AVR размер int тоже 16 бит.

Кстати, там же свое веселье с указателями. Нет, они обычные uint16_t числа, вот только адресация ОЗУ, ПЗУ (ну и EEPROM, но там-то ладно) не пересекается вообще никак. По значению указателя нельзя узнать указывает ли он на код или на данные.

В смысле, вместо int везде int64 делать?

И терять кучу тактов на ровном месте.

COKPOWEHEU
()
Ответ на: комментарий от qulinxao3

ибо Си это зерно гениев

Зерно в смысле seed (семя).

shdown ★★
()
Ответ на: комментарий от monk

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

win16 была приведена в качестве единственного известного мне примера где int<32bit

Slackware_user ★★★★★
()
Ответ на: комментарий от monk

в случае фортрана еще кстати есть замечательная фишка что там пока нет unsigned. т.е. если двуязычный код то в фортране везде обычный int нужной битности. а вот в C уже веселее после такого.

Slackware_user ★★★★★
()
Последнее исправление: Slackware_user (всего исправлений: 1)
Ответ на: комментарий от COKPOWEHEU

с AVR у меня с 1-2 взгляда была мысль что здесь приемлем только ассемблер.

Одно время заморачивался с мегами…

Slackware_user ★★★★★
()
Ответ на: комментарий от Slackware_user

Эм, в как же дос? Он намного популярнее чем win16, как ты его упустил?

firkax ★★★★★
()
Ответ на: комментарий от Slackware_user

главное чтоб вся работа с ним была в его либо большей размерности.

Так фиксированной битности под это условие нет. Вот 35 лет назад int всегда считался int16. Спустя ещё десяток лет может оказаться, что int всегда int128…

monk ★★★★★
()
Ответ на: комментарий от COKPOWEHEU

И терять кучу тактов на ровном месте.

А с чего вдруг? Регистры 64-битные, такты на работу с 64-битными числами такие же как и с 32-битными. Разве что кэш L1 быстрее кончится.

monk ★★★★★
()
Ответ на: комментарий от COKPOWEHEU

И терять кучу тактов на ровном месте.

Для Эльбруса (который тоже 64-битный) замена for(int ...) на for(long...) ускорила код в 6 раз.

monk ★★★★★
()
Ответ на: комментарий от monk

И терять кучу тактов на ровном месте.

А с чего вдруг?

Потому что у AVR, о котором шла речь, регистры 8-битные. Следовательно uint64 требует для хранения аж 8 регистров из 32 доступных. Это значит, что надо сначала куда-то сбросить предыдущее значение (281 тактов), если оно нужно, потом загрузить новое (еще 281 тактов), провести арифметическую операцию (в лучшем случае 8*1) и наконец сохранить обратно в память. И это для сложения и вычитания при увеличении количества регистров время будет расти всего лишь линейно, с умножением и делением все еще хуже.

Для Эльбруса (который тоже 64-битный) замена for(int …) на for(long…) ускорила код в 6 раз.

В том смысле, что ему пришлось добавлять лишние наложения масок чтобы вернуть int к 32-битной величине? Интересно, uint_fast32_t решило бы проблему?

А так да, совсем недавно на другом форуме тоже обсуждали, что не всегда стоит выбирать тип наименьшей разрядности. Именно из-за того, что компилятор будет вынужден добавлять проверки и ограничения диапазона. Я с подобным столкнулся на ARM и RISC-V.

COKPOWEHEU
()

Я не знток C. Но не понял, в чем претензия автора к C.

Если не понимаешь, как что-то устроено на более высоком уровне, смотри, как это устроено на более низком уровне, пока не дойдешь до формальной логики и схемотехники.

Когда дойдешь, всё, что выше, покажется абстракцией (а иногда и фикцией).

AnonymUser
()
Ответ на: комментарий от COKPOWEHEU

Потому что у AVR, о котором шла речь, регистры 8-битные.

Прошу прощения. Я думал, контекст всё ещё x86-64.

В том смысле, что ему пришлось добавлять лишние наложения масок чтобы вернуть int к 32-битной величине?

Нет. Механизм подкачки массивов требует 64-битные аргументы. Если счётчик 32-бит, то на каждой итерации надо делать преобразование 32->64 бит.

Интересно, uint_fast32_t решило бы проблему?

Да.

#if __WORDSIZE == 64
typedef unsigned long int       uint_fast16_t;
typedef unsigned long int       uint_fast32_t;
typedef unsigned long int       uint_fast64_t;
monk ★★★★★
()
Ответ на: комментарий от COKPOWEHEU

Интересно, uint_fast32_t решило бы проблему?

Нет. Решил бы int_fast32_t. C uint_fast32_t компилятору пришлось бы добавлять проверку на переполнение. Или доказывать, что в данном цикле переполнения быть не может.

ОБН: Хотя для 64 бит доказать обычно может. Решило бы.

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от AnonymUser

смотри, как это устроено на более низком уровне, пока не дойдешь до формальной логики и схемотехники.

… и узнаешь, как это реализовано в данном конкретном случае. А мало ли экзотических архитектур на свете. Вон выше были предположения, что int не может быть меньше 32 бит. Ну или может, но на совсем уж устаревших машинах. Впрочем, знание внутреннего устройства в любом случае лишним не будет.

Для этого и нужны техносрачи на форумах!

Нет. Решил бы int_fast32_t

Да, прошу прощения. Если в оригинале счетчик цикла был знаковым, то должен таковым и остаться. Хотя знаковый счетчик цикла не то чтобы частая штука…

COKPOWEHEU
()
Ответ на: комментарий от AnonymUser

Я не знток C. Но не понял, в чем претензия автора к C.

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

yorshka
() автор топика
Ответ на: комментарий от yorshka

Что-то новое. Хотя ругать С стало модным.

C(89) как раз довольно четко стандартизирован. Осмелюсь добавить, математически или логически выверен, с ясной мнемоникой.

Даже код, который писал я, на самых стандартных библиотеках,без изменений запускал на разных архитектурах и системах, а я не очень-то программист.

Все-таки усматриваю что-то личное к С у Вас или идеологическое.

Если бы в языке были бы действительно противоречия или изъяны, их бы давно заметили. А так, консольные программы все ещё пишут на C даже любители, не говоря о тех кто работает на компании - системщики. Хотя не знаю, конечно, что там в реальности, в ком. сфере, в крупных компаниях… Только слухи…

C создан в 70-х годах, стандартизирован в 89. Хороший срок для обкатки. Сейчас 26 год. На всякий случай. На нем написан Minix. Он в общем закрепился в научном сообществе. Воспринимается как псевдокод. Тут можно только смириться…

AnonymUser
()
Последнее исправление: AnonymUser (всего исправлений: 5)
Ответ на: комментарий от yorshka

из-за чего вылезает просто вагон проблем

Нет, «вагон проблем» вылезает из-за того, что кто-то решил доверять графоманским документам в качестве истины. Задача Си - привести более-менее к универсальному виду прогание под разные платформы, при этом по возможности не потеряв индивидуальные преимущества и просто особенности каждой. Подчёркнутое - важно. Полная унификация приведёт к потере особенностей платформ, а это как раз то, чего допускать нельзя.

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

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.