LINUX.ORG.RU

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

 , , ,


2

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

time_t вроде как изначально был int32_t, сейчас int64_t, как по мне в прикладном софте его лучше не трогать пока все на 64 не перейдут (а до 32 года не так далеко)

Ну либо как извращенный вариант хранить в int64_t и кастовать в time_t в надежде что компилятор современный(подобие шутки))

Долговато я что-то тупил чтоб это вспомнить))

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

Ничего не понял, что там «надо смотреть». Компиляторы для доса 16-битные, int там тоже 16-битный (или у тебя по каким-то причинам этот общеизвестный факт вызывает сомнения?). Расширители это другое, я про них не думал, но на факт 16-битности обычного дос-софта они никак не влияют.

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

Я не уверен что во всех моделях памяти у ваткома int==short

согласно ману у них 2 типа интов - long и short, для 16 бит short == int

Подробнее не вчитывался если честно.

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

Во-первых, почему такое акцентирование на ваткоме? Дефолтный си-компилятор для доса скорее турбо си. Во-вторых, на 16-битных платформах делать int не 16-битным было бы крайней степенью идиотизма, вне зависимости от оправданий. Потому как такой компилятор бы тайпкастил всю арифметику (см. integer promotion) к неудобной для проца разрядности и делал бы таким образом на ровном месте тормозящие в несколько раз проги, не говоря уж про такие мелочи как двукратный жор памяти на все int-ы тоже на ровном месте. А ещё int раньше позиционировался как наиболее нативный для проца тип, с этим тоже расхождение получится. Сейчас это уже нарушили (на 64бит платформах), но тут нарушили ради того чтобы не сломалась куча старого кода. А зачем это было бы делать там не представляю.

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

time_t вроде как изначально был int32_t, сейчас int64_t

Так time_t вроде для того и изобрели, чтобы унифицировать uint32 / uint64

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

С time_t вообще странно, в старых функциях имеется тенденция передавать его указателем (time(&t), localtime(&t)), как будто что-то может не позволить его передать значением. А ещё я в одном старом коде (до-amd64) видел вычисление разницы двух time_t с кастом или кастами к double.

Не удивлюсь если где-то в древности time_t был вообще не целочисленным типом, а дробным или структурой, или хотя бы были такие планы.

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

видел вычисление разницы двух time_t с кастом или кастами к double.

Что за извращение.

Не удивлюсь если где-то в древности time_t был вообще не целочисленным типом

Вот это вряд ли. Уж от Си-шников такое никак не ожидаешь.

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

Не удивлюсь если где-то в древности time_t был вообще не целочисленным типом, а дробным или структурой, или хотя бы были такие планы.

С плавающей точкой в стандарте точно был. Ещё в 2001 году

Only
     the following properties are guaranteed by the IEEE Std 1003.1-2001
     ("POSIX.1") standard:
...
           4.   The time_t and clock_t types are either integers or real-
                floating types.  

(https://www.daemon-systems.org/man/types.3.html)
monk ★★★★★
()
Ответ на: комментарий от monk

А ещё есть замечательное:

double difftime(time_t tim1, time_t tim2);

Разница в секундах между двумя time_t должна быть типа double.

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

А, так это стандартная функция. Вот где я double и видел.

А в мане к ней вот что:

NOTES
       On a POSIX system, time_t is an arithmetic type, and one could just de‐
       fine

           #define difftime(t1,t0) (double)(t1 - t0)

       when the possible overflow in the subtraction is not a concern.
То есть, в каких-то non-POSIX системах, time_t могло быть не числовым.

Хотя смысл делать double для ответа есть даже если time_t - signed int. Потому что вычитая два int-а можно получить переполнение, а вот после кастования их (ещё до вычитания) к double переполнения уже быть не сможет.

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

Какую тему я пропустил! ТС упоролся, это как критиковать что вода слишком мокрая

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

Все известное из игрушек под дос компилилось Watcom.

Большая часть софта прикладного был masm.

Turbo Pascal живьем видел, а вот Turbo C не.

Был еще microsoft c, там типы как для win16 само собой

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

signed int в 2032 станет тыквой, хотя смысл каста в дабл результата все одно не вижу.

зачем время в floating point?

еще б деньги float считать…

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

типы как для win16 само

Да что ж такое опять!? win16 это поздняя оболочка для доса! Это в «win16 типы как в досе16», а не наоборот.

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

signed int в 2032 станет тыквой, хотя смысл каста в дабл результата все одно не вижу.

Каст результата в double - только для совместимости. А вот оригинальное difftime могло и что-то другое делать. Каст операндов в double перед вычитанием чтобы не получить чушь из INT_MAX-INT_MIN. То, что отрицательные timestamp обычно не используются - не повод делать чтобы твоя прога от них глючила.

зачем время в floating point?

Вероятно затем что бы доли секунд учитывать без timeval/timespec и прочего мазохизма. Тут в целом три варианта, и у всех есть недостатки:

1) timeval/timespec, где доли секунды в отдельном целом типе, недостаток - это не арифметический тип получается, его нельзя без возни складывать/вычитать итд (возможно кстати какие-то древние time_t такими и были, от того и процитированное пояснение из мана)

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

3) счётчик милли/микро/наносеунд одним целым числом (уже 64-битное если надо реальное время так хранить), недостаток - невозможность просто прочитать из него секунды без делений, а так же мучительные размышления на счёт того, как обрабатывать врап, который исходно то врап и не ломает арифметику, а вот после деления на например 1000000000 будет уже багопровоцирующим закруглением после приблизительно 18.4 миллиардов секунд.

firkax ★★★★★
()

Мне казалось это всем известно, они не наркоманы, они алкоголики. Только для С/С++ вычислена точка Балмера.

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

microsoft c позже win 3.1 если правильно помню. До этого был quick c а по факту масм и гвбейсик.

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

Ну double то точность теряет поскольку дискретный достаточно сильно.

Unix time в секундах, есть вроде бы апи дергающее кратные части секунды, но там все равно целые

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

Ну double то точность теряет поскольку дискретный достаточно сильно.

Чего?

Unix time в секундах, есть вроде бы апи дергающее кратные части секунды, но там все равно целые

Вот смотри, я пишу:

1) timeval/timespec

ты отвечаешь:

есть вроде бы апи ... кратные части секунды

вроде

Это ты дотуда не дочитал или ты не в курсе как называется структура для времени с точностью до микро/наносекунд?

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

Последние лет 7 фортран\паскаль\питон. Потихньку начинаю забывать.

Столярова к своему стыду далее середины 1 тома пока некогда.

Потому и ВРОДЕ.

дабл весьма дискретный для отдельной степени, но лучше нет ничего адекватного.

float использовать конечно можно но только там где числа вообще ни на что не влияют.

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

К тому что float - имитация вещественных чисел. И при определенных условиях дает дичь. И сделать с этим что либо сложно.

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

float и double это разные типы, у первого 24 значащих бита, у второго 53.

Никакой из них не имитация. Про «весьма дискретность» до сих пор непонятно.

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

Ну double то точность теряет

Он теряет точность если надо выравнивать порядки. А пока не кончатся биты мантиссы, в этом нет нужды. Длина мантиссы у double 53 бита. То есть int53_t. Если пока под секунды хватает 32 бит, на дробную часть останется 21 бит. Дискретность примерно пол-микросекунды.

Это не отменяет неоправданные сложности использования дробных чисел в качестве простого счетчика.

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

Для счетчиков все равно плохо. Часть битов теряется впустую, сами вычисления существенно более сложные (особенно если нет FPU). Да еще и точность может внезапно измениться.

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

сишечка не гарантирует что реализация обязательно использует 2’s complement формат?

Сишечка ещё за C++ не подтянулась? Там сейчас это на уровне стандарта.

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

Си не гарантирует 2's complement, однако тайпкасты signed <-> unsigned должны его эмулировать. Столкнуться с особенностями формата ты сможешь если будешь тайпкастить указатели: *(int*)&uint_var

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

Си не гарантирует 2’s complement, однако тайпкасты signed <-> unsigned должны его эмулировать.

Знаешь, я на си уже давно пишу, в том числе за деньги, но до сих пор поражаюсь способности авторов языка сделать даже такие банальные вещи через жопу.

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

А тут на самом деле всё логично.

Если ты тайпкастишь -1 в unsigned - будет удобно если получится что-то, что совпадает с результатом вычитания единицы из unsigned нуля через врап. А это как раз совпадает с 2's complement представлением. В итоге на большинстве архитектур этот тайпкаст вообще no-op, а на редких с другими отрицательными числами - доп. операции, зато сохраняют совместимость высокоуровневой арифметики.

Ну и конструкция ((unsigned something)-1) для получения максимального (все единичные биты) числа заданного типа удобнее чем ((unsigned something)~(unsigned something)0) которую бы пришлось иначе использовать.

Ну и ты ж не требуешь чтобы тайпкаст float -> int32 делался на основе их битовых представлений.

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

Ну и конструкция ((unsigned something)-1) для получения максимального (все единичные биты) числа заданного типа удобнее чем ((unsigned something)~(unsigned something)0)

все единичные биты

firkax. firkax никогда не меняется.

unsigned var1 = ~0u;
unsigned long var2 = ~0ul;
unsigned long long var3 = ~0ull;
r--r--r--
()
Последнее исправление: r--r--r-- (всего исправлений: 1)
Ответ на: комментарий от r--r--r--

И как мне из названия типа, если он не один из перечисленных, получить этот суффикс? Например, для size_t, и без всяких предположений вида «наверно он той же разрядности что long»?

И ещё учти, что для типов меньше int так не полуичится, ~ скастует его в int и результат будет единичные биты int-а. Так что без двух кастов никак. Но, хорошо, есть способ через минус единицу.

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

Например, для size_t, и без всяких предположений

size_t и компания - implementation defined, поэтому смотришь доку или :

    size_t sz = 0;
    sz =~ sz;
r--r--r--
()
Ответ на: комментарий от firkax

И ещё учти, что для типов меньше int так не полуичится, ~ скастует его в int и результат будет единичные биты int-а.

Господи, что за херню ты несёшь?

#include <stdio.h>

int main(void)
{
    unsigned char var = (unsigned char) ~0u;

    printf("%zu : %X\n",   sizeof(var), var);

    return 0;
}
$ gcc -pedantic unsigned_char.c -o unsigned_char
$ ./unsigned_char 
1 : FF

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

Господи, что за херню ты несёшь?

Нет, ты. Ты глупый или притворяешься? Задача была выразить указанную величину для произвольного типа. И теперь сравни:

1) то, что реально применяется

(typename)-1

2) то, что пришлось бы применять если бы не было указанной логики тайпкастов

(typename)~(typename)0

3) то, что предлагаешь ты

typename varname = 0;
varname = ~varname;

Нафиг ты несёшь эту чушь, когда очевидно, что первый вариант удобнее вторго, а твой третий - ещё длинее чем второй и вообще требует дополнительное объявление переменной?

И следующее

unsigned char var = (unsigned char) ~0u;
ты контекст потерял или как? Мы не знаем что за тип, не знаем какая у него разрядность. И size_t там был в качестве примера, представь теперь qwert_t вместо него. И опять лишнюю переменную объявил. Перед тем как придираться изучи вопрос, а то клоунада получается.

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

И ещё учти, что для типов меньше int так не полуичится,
И следующее
unsigned char
ты контекст потерял или как?

Да-да, я потерял. В самом деле, ну не ты же? Ты ведь не можешь его потерять в буквально одном посте. Ведь, правда, не можешь?

Нафиг ты несёшь эту чушь, когда очевидно,

Давай, до свидания.

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

Подобные решения были связаны с неполной поддержкой 64-х битных челых чисел на 32-х битных системах. Например, деление не работало, из-за чего их печать не работала.

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

Подобные решения были связаны с неполной поддержкой 64-х битных челых чисел

Которые какое отношение имеют к 32-ух битному time_t?

(Я в курсе, да.)

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

Я о том, чтобы они написали уже требование 2’s complement и не насиловали никому разум. Всё равно других архитектур не осталось.

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

Тут смотри что получается. Если ты уверен, что других архитектур не осталось, то «требование 2's complement» выполняется автоматически вне зависимости от спецификаций Си, и соответственно тебе не о чем беспокоиться. Если же такие архитектуры вдруг всё-таки остались или появятся новые, то данное требование эквивалентно «для этих архитектур нельзя ничего делать на нашем Си, если только вы не сделаете поверх них 2's complement виртуалку». Выглядит это крайне сомнительно, поэтому лучше всё-таки данное требование не заявлять.

Хотя, с другой стороны, наличие или отсутствие данного требования в каких бы то ни было документах всё равно ни на что не повлияет: если кто-то захочет написать Си-компилятор для такой архитектуры, он всё равно это сделает, и требование там выполняться всё равно не будет.

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

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

Тут смотри что получается. Если ты уверен, что других архитектур не осталось, то «требование 2’s complement» выполняется автоматически вне зависимости от спецификаций Си, и соответственно тебе не о чем беспокоиться.

Понимаешь, в чём штука. Знаю, что это твоя любимая тема, но отсутствие требования 2’s complement – единственная причина, почему знаковое переполнение не определено и почему компилятор считает, что для знаковых чисел выражение i + 1 > i всегда будет верным, а значит его можно оптимизировать. И GCC и шланг так всё время делают по умолчанию.

Если же такие архитектуры вдруг всё-таки остались или появятся новые, то данное требование эквивалентно «для этих архитектур нельзя ничего делать на нашем Си, если только вы не сделаете поверх них 2’s complement виртуалку». Выглядит это крайне сомнительно, поэтому лучше всё-таки данное требование не заявлять.

Только они не появятся, потому что подавляющая часть сишного софта без -fwrapv не работает (либо работает некорректно), что фактически аналогично требованию 2’s complement. Добавление требования 2’s complement просто сделает это поведение дефолтным.

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

но отсутствие требования 2’s complement – единственная причина, почему знаковое переполнение не определено и почему компилятор считает, что для знаковых чисел выражение i + 1 > i всегда будет верным, а значит его можно оптимизировать

Исторически - может быть. Сейчас это тянут по-моему ради оптимизаций как раз, и даже при наличии требования 2's complement не уберут. Так что просто ставим -fwrapv и всё норм. Может быть, потом будет совесть мучать «а вдруг мой код скомпилируют на той странной платформе и он там сломается, и я буду виноват»? А так ты ей ответишь «да не, мне ISO-комитетчики разрешили так делать, а виноват тот кто компилирует на несовместимую платформу».

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

но отсутствие требования 2’s complement – единственная причина, почему знаковое переполнение не определено и почему компилятор считает, что для знаковых чисел выражение i + 1 > i всегда будет верным, а значит его можно оптимизировать

Наоборот же. Единственная причина, почему знаковое переполнение не определено и почему компилятор считает, что для знаковых чисел выражение i + 1 > i всегда будет верным, это чтобы его можно было оптимизировать.

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

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

Наоборот же. Единственная причина, почему знаковое переполнение не определено и почему компилятор считает, что для знаковых чисел выражение i + 1 > i всегда будет верным, это чтобы его можно было оптимизировать.

Тогда почему беззнаковое переполнение определено?

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

Круто. Почему для беззнакового это не используется?

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

Тогда почему беззнаковое переполнение определено?

Компромисс. Для unsigned char и unsigned int часто используется арифметика по модулю 2^n.

Поэтому для них переполнение определено и поэтому for(int i ...) часто работает быстрее, чем for(unsigned i ...).

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

Тогда почему беззнаковое переполнение определено?

Компромисс.

И это никак не связано с тем, что разные ISA могли использовать разные форматы для отрицательных чисел? Ну вообще никак?

Поэтому для них переполнение определено и поэтому for(int i …) часто работает быстрее, чем for(unsigned i …).

шта

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

И это никак не связано с тем, что разные ISA могли использовать разные форматы для отрицательных чисел? Ну вообще никак?

Могли поставить unspecified behaviour. Это как раз «на разных ISA по-разному, но в рамках одного ISA/компилятора единообразно». Но выбрали undefined behaviour, то есть «компилятор может считать, что такого не бывает».

шта

Для for(int i = x; i != y; i++) { ... a[i] ... } есть гарантия, что массив a читается последовательно (можно предсказуемо подгружать кэш и использовать всякие SIMD).

Для unsigned всё становится заметно сложнее: либо делать проверку и удваивать объём кода, либо не иметь гарантии последовательного чтения.

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

И это никак не связано с тем, что разные ISA могли использовать разные форматы для отрицательных чисел? Ну вообще никак?

Могли поставить unspecified behaviour. Это как раз «на разных ISA по-разному, но в рамках одного ISA/компилятора единообразно». Но выбрали undefined behaviour, то есть «компилятор может считать, что такого не бывает».

Unspecified против Undefined – это вообще очень странная штука. Есть ощущение, что авторы стандарта пихали undefined просто так на каждый чих, особенно учитывая штуки типа отсутствия переноса строки в конце файла.

Для for(int i = x; i != y; i++) { … a[i] … } есть гарантия, что массив a читается последовательно (можно предсказуемо подгружать кэш и использовать всякие SIMD).

Такая гарантия есть только потому что переполнение является UB. То есть гарантии нет.

Опять же, большая часть сишного кода собирается с -fwrapv, поэтому переполнение в любом случае определено.

Для unsigned всё становится заметно сложнее: либо делать проверку и удваивать объём кода, либо не иметь гарантии последовательного чтения.

Нет, всё проще. Если цикл гарантированно завершается, то никаких доп.проверок не нужно.

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

большая часть сишного кода собирается с -fwrapv

Большая часть сишного кода где? Точно не в юзерспейсном OSS-софте.

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

Такая гарантия есть только потому что переполнение является UB. То есть гарантии нет.

Оптимизатор гарантирует, что поведение корректной программы не изменится. Программист гарантирует, что программа корректна, то есть в ней нет UB (а если есть, то программисту в этом случае результат не важен).

Опять же, большая часть сишного кода собирается с -fwrapv, поэтому переполнение в любом случае определено.

Это диалект Си с доопределённым UB. Оптимизация работать перестанет, зато можно будет делать переполнение знаковых переменных.

Если цикл гарантированно завершается, то никаких доп.проверок не нужно.

В смысле? Вот цикл for(unsigned char i = x; i!= y; i++) { ...a[i]... }. Он завершается. Но оптимизировать такой цикл нельзя в прочитать в кэш a[x..y-1]; обработать последовательно. Потому что может быть x = 200, y = 5 и тогда надо дробить на прочитать в кэш a[x..255]; обработать последовательно; прочитать в кэш a[0..4]; обработать последовательно;. Или вообще забить на оптимизацию и читать в кэш линию при чтении очередного элемента массива.

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