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)
Ответ на: комментарий от anonymous

увеличенный до 0x80000001

*До 0x800000010

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

Если ты спросишь лично моё мнение по этому вопросу, то я считаю, что в языке не должно быть UB в принципе.

Так всем тогo же хотелось бы, понятное дело. Какие-то «странности» можно было и доопределить давно, верно. Но что ты предлагаешь делать, например с:

int i, j, k;
i = 42;
j = 32;
k = i >> j; 
/* sizeof(int) = 4 */

Ммм? Сдвиг сегодня всюду выполняется за 1 или даже 0 тактов, но вот в граничных случаях на разных архитектурах будет разное. Где-то no-op, где-то k = 0. А как доопределять будем? Проверкой в рантайме? Представляешь, что начнётся? Собираешься лишать людей любимого занятия биты-байты-такты оптимизировать? Ну держись!😁

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

Но, вижу тебе табличка понравилась, вот ещё «прикол»

Не по сезону клевер топчешь.

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

А как доопределять будем? Проверкой в рантайме? Представляешь, что начнётся?

Ну на расте так и делают. Правда, не проверкой, а переписыванием i >> j в i >> (j & 31). Но вот SIMD-сдвиги на x86-64 зануляют, а не берут по модулю. Насколько я понимаю, векторизовать LLVM уже не может такое.

shdown ★★
()

Все логично сделано

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

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

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

Ммм? Сдвиг сегодня всюду выполняется за 1 или даже 0 тактов, но вот в граничных случаях на разных архитектурах будет разное. Где-то no-op, где-то k = 0.

У тебя код содержит ошибку, поэтому результат зависит не то что от архитектуры процессора, а от конкретного компилятора и флагов сборки. Не надо так писать. Это плохо.

А как доопределять будем? Проверкой в рантайме? Представляешь, что начнётся? Собираешься лишать людей любимого занятия биты-байты-такты оптимизировать? Ну держись!

Предлагаю написать в стандарте, что поведение зависит от реализации, но не является неопределённым.

yorshka
() автор топика
Ответ на: комментарий от firkax
[~]-[%] clang -v
Debian clang version 21.1.8 (7+b1)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/lib/llvm-21/bin
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/13
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/14
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/15
Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/15
Candidate multilib: .;@m64
Selected multilib: .;@m64
[~]-[%] cat foo.c

#include <stdio.h>

int main(int argc, char **argv)
{
    (void) argv;

    int i, j, k;
    i = 42;
    j = 32;
    k = i >> j;

    if (k == 0) {
        k = argc | 1;
    }
    printf("%d\n", k);
    return 0;
}

[~]-[%] clang -O3 foo.c -o foo
[~]-[%] ./foo; echo "Exit code: $?"
Exit code: 40
[~]-[%]

Как объяснишь, ммм?

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

An expression is shifted by a negative number or by an amount greater than or equal to the width of the promoted expression

Это UB.

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

это не UB.

Я в тебе ни секунды не сомневался.

6.5.7 Bitwise shift operators
-8<- -8<- -8<-
If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined.

Да и сама формулировка

Результат сдвига может быть разный в зависимости от платформы - это не UB

Это отдельный кек.🤖

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

У тебя код содержит ошибку, поэтому результат зависит не то что от архитектуры процессора, а от конкретного компилятора и флагов сборки. Не надо так писать. Это плохо.

Друг, ты меня не так понял. Мой код этот момент предусматривает и использует только определённое поведение, но как компилятор об этом узнает?

Если кто-то решится переопределить сдвиг так, чтоб не было UB, то ему придётся или проверку в рантайме делать или, как предложили выше, помещать операнд в строгие границы. Так или иначе даже для тех случаев, когда код верный появятся дополнительные операции.

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

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

Предлагаю написать в стандарте, что поведение зависит от реализации, но не является неопределённым.

Но чем тебе, как программеру на языке C, это поможет? Не выльется ли твоя задумка в С без единого UB в итоге в «язык-с-x86_64», «язык-с-armv8», «язык-с-rva23» и т.д.?

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

Друг, ты меня не так понял. Мой код этот момент предусматривает и использует только определённое поведение

Нет. Сам же выше цитату привёл.

но как компилятор об этом узнает?

Компилятору не обязательно об этом «знать». Компилятор при работе с UB исходит из предположения, что корректной программе на Си его нет. Т.е. что правый операнд в сдвиге всегда положительный и при этом меньше чем количество бит в левом. Т.е. в следующем коде

int i, j, k;

i = something();
j = something_else();
k = i << j;

if(j >= sizeof(i) * CHAR_BITS)
  die("Shift overflow");

проверка вполне может быть выкинута.

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

Не выльется ли твоя задумка в С без единого UB в итоге в «язык-с-x86_64», «язык-с-armv8», «язык-с-rva23» и т.д.?

Си уже примерно в таком варианте и существует. Например, многим при переносе кода с x86_64 на AArch64 жопы поотстреливало из-за другой модели работы с памятью.

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

Ну хорошо, а это поведение gcc как объяснить?

https://godbolt.org/z/fP3Gs6zzb

(Если здесь не нравится __attribute__((always_inline)), big_scary_func можно заменить на макрос.)

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

Сишка deprecated. Используй rust. Rust даже в ядро затащили. Нет никакого оправдания, чтобы не использовать rust в user space.

ox55ff ★★★★★
()

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

Нет, просто они написали платформонезависимый ассемблер, где за всеми отстрелами ног программист следит сам. Это надо просто учитывать. Для написания ядра – годится (и то не все с этим согласны), для написания прикладного кода – с большими оговорками.

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

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

Хм, неожиданное поведение, не совсем консистентное. Но по крайней мере соседний код он не портит.

inline там не требуется, и вообще пример можно сильно сократить

https://godbolt.org/z/19n9GeqK9

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

При чём тут макросы? Они только для более наглядного исходника, на компиляцию никак не влияют.

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

просто они написали платформонезависимый ассемблер

Во-первых, сам по себе термин «платформонезависимый ассемблер» – это чудовищный оксюморон. Во-вторых, к ассемблеру сишка не имеет практически ровно никакого отношения, за исключением пары артефактов от ассемблера PDP-11. В третьих, сишка – это на самом деле весьма высокоуровневый язык, иначе бы компилятор не оптимизировал программы настолько сильно, чтобы даже @firkax офигивал от увиденного.

И в приведённом листинге треть кода это вообще не Си, а макросы препроцессора,

Препроцессор – часть языка Си. Он описан в стандарте языка, реализация является частью компилятора.

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

Препроцессор – часть языка Си.

4.2

Если бы препроцессор был "частью языка Си", его нельзя было бы использовать отдельно от языка Си. Его можно использовать отдельно от языка Си. Следовательно, препроцессор не является частью языка Си.

Он описан в стандарте языка, реализация является частью компилятора.

Частью toolchain’а и инфраструктуры разработки. Так же как компоновщик. Ты же не будешь утверждать, что компоновщик является "частью языка Си"?

Я уж не говорю о том, что синтаксически это два разных языка с разными правилами.

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

Если бы препроцессор был «частью языка Си», его нельзя было бы использовать отдельно от языка Си.

Кто тебе такую чушь сказал? Плюнь ему в лицо. Тебя обманули.

Я уж не говорю о том, что синтаксически это два разных языка с разными правилами.

Правильно, лучше не говори, а то курам на смех поднимут. Особенно учитывая, что такая ситуация встречается буквально повсеместно. На C++ вон вообще налепили отдельный недохаскелл и назвали шаблонами.

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

синтаксически это два разных языка с разными правилами.

Формально ты прав, но по факту что? «Дровами топят печку. Эти дрова - сами по себе дрова, можете ими топить что угодно. А, ну да….»

wirewalk
()
Ответ на: комментарий от ugoday

сишка – это на самом деле весьма высокоуровневый язык

Ну и кто тут наркоман теперь?

Ты? В сишке нет ничего низкоуровневого: ни доступа к SIMD, ни управления кешами, ничего.

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

В сишке нет …

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

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

Я уж не говорю о том, что синтаксически это два разных языка с разными правилами.

Правильно, лучше не говори, а то курам на смех поднимут.

Покажи мне того дебила, кто будет утверждать, что препроцессор и сам си - это один и тот же язык с идентичным синтаксисом.

Особенно учитывая, что такая ситуация встречается буквально повсеместно.

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

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

В Java тоже нет сопоставления с образцом. И что - низкоуровневый язык теперь? Всё относительно. Для какого-нибудь инженера Intel, который работает с микрокодом, и ассемблер будет высокоуровневым, наверное. А для какого-нибудь начальника все эти языки программирования это недопустимо низкий уровень, а помахать руками, издавая междометия и слабо связанные словосочетания - вот это нормально.

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

«платформонезависимый ассемблер» – это чудовищный оксюморон

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

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

и платформонезависимые свойства.

Сможешь ли ты явить миру ассемблер полностью из "платформонезависимых свойств" без явного указания целевой платформы? Будет ли такая программа программой ассемблера?

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

В сишке нет …

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

Всё это не значит, что сишечка низкоуровневый язык. Это значит, что сишечка – убогий язык.

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

Покажи мне того дебила, кто будет утверждать, что препроцессор и сам си - это один и тот же язык с идентичным синтаксисом.

Думаю, на ЛОРе такой найдётся, но это буду не я. Я тут утверждаю, что препроцессор – ЧАСТЬ языка Си, причём достаточно неотъемлемая, потому как я ещё не видел программ на Си, не использующих препроцессор.

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

В Java тоже нет сопоставления с образцом.

Прискорбно. Это понижает уровень явы. А в С нет вообще ничего. Только указатели на буфера в памяти и арифметика для них. О чём тут вообще спорить?

Всё относительно.

Относительно героиновых наркоманов травокуры почти не употребляют. Вот так же и с «высокоуровневым» С.

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

Вот так же и с «высокоуровневым» С.

Окей. Является ли Brainfuck низкоуровневым? Потому что у тебя определение «высокоуровневого» и «низкоуровневого» языка отличается от моего, похоже.

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

JVM bytecode

Прикольно, заставил задуматься: а это ближе к intermediate representation или к ассемблеру? Да, о существовании разных попыток процессоров java знаю, но есть ли там внутри 1:1 соответствие инструкций? Или же идет разбор каждой на вагон и тележку внутренних операций, хотя.. кто сейчас так не делает.

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

Я тут утверждаю, что препроцессор – ЧАСТЬ языка Си

Но доказательств не предоставил.

я ещё не видел программ на Си, не использующих препроцессор.

Это значит, что ты видел очень мало программ на Си, только и всего лишь.

void f(){}

/thread

r--r--r--
()
Ответ на: комментарий от beefdeadbeef

Держи triforceлайк за стишок, аву и псевдоним. ദ്ദി(ᵔᗜᵔ)

anonymous
()
Ответ на: комментарий от r--r--r--

Но доказательств не предоставил.

Разупорись и сходи стандарт почитай.

Это значит, что ты видел очень мало программ на Си, только и всего лишь.

void f(){}

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

Кстати, стандарт Си требует, чтобы в программе на Си была функция main() (в случае hosted environment).

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

Является ли Brainfuck низкоуровневым?

Да, если запустить его на слегка модифицированной машине Тьюринга.

определение «высокоуровневого» и «низкоуровневого» языка отличается от моего, похоже.

Для ясности, я использую следующую понятийную базу: если программист думает в терминах решаемой задачи — язык высокоуровневый. Если в терминах устройства, на котором код будет исполняться — низкоуровневый. Примеры: 1С — язык высокого уровня, т.к. 1С-программист оперирует понятиями налогового, складского учёта, КЗОТ и т.п. С — язык низкого уровня, т.к. оперирует понятиями устройства процессора. Всё, конечно, относительно и все полные по Тьюрингу языку теоретически эквивалентны. Но чтобы быть успешным в 1С нужно знать предметную область, а в С — как правильно выровнять данные в структурах, чтобы минимизировать промахи по кэшу. И в этом меж ними колосальная разница.

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

Это значит, что сишечка –

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

wirewalk
()
Ответ на: комментарий от beefdeadbeef

Отличная, кстати, мысль для собеседований. Жаль работает только очно, а это сейчас не модно. Заходит кандидат в офис, перед ним нужно сишку на пол бросить. И смотреть, перешагнёт или поднимет.

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

Является ли Brainfuck низкоуровневым

Он является мозголомным, по определению. Интуитивно мы ожидаем баланса между пользой и мозголомностью. Если язык приносит несомненную пользу в нашем нелегком деле, то за это мы готовы платить некоторой мозголомностью. Brainfuck это хороший пример «заплатить много ни за что».

«Уровневость» - это не та шкала, плохо подходит для описания реальности и вводит в заблуждение. Несомненно есть то, что называется DSL. То есть язык, максимально заточенный под предметную область. Есть максимально универсальные и гибкие языки, которые можно приспособить практически к чему угодно, и превратить их в некоторое подобие DSL.

Более практичный, в общем случае, подход - лепим на «низкоуровневом» языке DSL, а на нем решаем прикладную проблему. Иногда оправдана и трехуровневая система (если DSL сложен, потому что предметная область такая) - сначала наш ЯП, потом сверху «высокоуровневый» (типа js или питон) затем уже DSL.

Вот как «уровневость» определяется правильно. Кто лежит в более низом уровне этой пирамиды, тот и «низкоуровневый», кто повыше, тот и «высокоуровневый». А «уровневость» в отрыве от этой конструкции - это сферический конь.

wirewalk
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.