Pull to refresh

Comments 26

Ну я человек простой: рекомендуют проверить — я проверяю. Чтобы не отставать от хайпа, я реализовал акторную модель правильно (как она реализована в эрланге) — и понял, что на этом моё знакомство с растом и закончится: мне визуально не нравится синтаксис, а этого достаточно, чтобы себя от него изолировать.

Но библиотеку я опубликовал, ей даже кто-то пользуется, надо проверить, решил я.

test epmd::server::tests::test_handle_list_nodes ... error: unsupported operation: `clock_gettime` with `REALTIME` clocks not available when isolation is enabled
   --> /home/am/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/pal/unix/time.rs:107:22
    |
107 |         cvt(unsafe { libc::clock_gettime(clock, t.as_mut_ptr()) }).unwrap();
    |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported operation occurred here
    |
    = help: set `MIRIFLAGS=-Zmiri-disable-isolation` to disable isolation;

Ну как так-то? Нет, MIRIFLAGS=“-Zmiri-disable-isolation” cargo +nightly miri test прошёл, UB у меня, вроде, нет. Но что-то не так в стандартной библиотеке, в коде, который отвечает за операции со временем (!). Серьёзно?

Всё с ним норм, явно же написано что unsupported operation - miri не может проверить системный вызов времени. Miri с выключенным isolation требует детерминизма в коде.

И я не совсем понимаю почему вы используете SystemTime::now(), а не Instant::now() для отсчёта временных интервалов. Но скорее всего последний поддерживается в Miri.

И не пытайся восстановить указатель из числа через as, если у тебя нет провенанса под рукой, для этого с недавних пор есть with_exposed_provenance, и это не косметика, а единственный задокументированный способ сделать такое без UB.

Тем временем документация на with_exposed_provenance:

This is fully equivalent to addr as *const T
https://github.com/rust-lang/rust/blob/main/library/core/src/ptr/mod.rs#L970

и эти правила строже, чем в C

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

Про UB ещё можно рассуждать, что мы в конечном счёте имеем дело с LLVM, а тот эксплуатирует UB как обычно. Когда LLVM выкидывал пустые бесконечные циклы - это отразилось на всех фронтендах, на C, C++, Rust - хотя с точки зрения Си и раста он на это права не имел.

Я нарывался в Rust на UB при операциях с числами с плавающей запятой из-за оптимизации LLVM. Одно и то же выражение на ПК оказывалось не нулевым, а на МК - нулевым. Просто из-за перестановки местами множителей при оптимизации. При сохранении порядка умножений, как в исходном выражении, всё было хорошо. А при оптимизации при промежуточном умножении возникало исчезновение порядка и результат оказывался нулевым.

О том, что результат операции над float нельзя сравнивать с целым, включая 0, мне рассказывали на лекции в году эдак 2003-ем. Последний бит в операции над float является округлённым и может встать так или сяк в зависимости от того, какой именно командой он делался. Ещё не забываем, что множество двоичных float определённого размера не совпадает с множеством десятичных. То есть в переменной хранится число, наиболее близкое к тому, которое подразумевается по логике - из множества тех чисел, которые могут в ней храниться.

Всё это не Undefined behaviour. Это очень даже подробно defined, в документе от IEEE под номером знать не хочу каким, потому что в быту всё оно прекрасно сводится к фразе "последний бит дробного числа зашумлён"

О том, что результат операции над float нельзя сравнивать с целым, включая 0, мне рассказывали на лекции в году эдак 2003-ем.

Вы явно перепутали сообщение, на которое отвечаете. Сравнение с нулём не имеет вообще никакого отношения к исчезновению порядка.

Одно и то же выражение на ПК оказывалось не нулевым, а на МК - нулевым.

???

Именно так. На ПК было что в районе 100, а на МК - чистый ноль.

Исходя из того, что Вы явно впервые слышите об исчезновении порядка, поясняю:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=c67e00613b210f506d07be08e60a9596

fn main() {
    let a:f32 = 5E-30;
    let b:f32 = 4E-30;
    let c:f32 = 3E30;
    let d:f32 = 2E30;
    println!("a*b*c*d = {}, a*c*b*d = {}", a*b*c*d, a*c*b*d);
}

Если оптимизатор LLVM решает умножать сначала a на b, например, потому что они уже в регистрах, а свободных регистров в МК нет, то получит в итоге 0, но если будет умножать в указанном в коде порядке (a*c*b*d), то получится 119.99999. Как это ещё назвать, кроме как UB?

Понятно, что если отключить fast-math у LLVM, то проблема исчезнет. Но ценой снижения производительности кода. Выбирайте.

Спасибо, я над этим поэкспериментирую.

Если честно, я бы предпочёл, чтобы Rust, хотя бы при указании соответствующих флагов, паниковал при исчезновение порядка при операциях с числами с плавающей запятой (floating-point underflow). Но пока воз и ныне там, приходится лазить в дебагере, разбираясь с такими чудесами.

Насколько мне известно, в расте не используется fast-math. Есть настабильный аналог, но он opt-in.

Касательно разного поведения и порядка вычислений. От того, что у Вас разные результаты ещё не значит, что у Вас уб. И в расте не планируют делать уб для флотатов...

Насколько мне известно, в расте не используется fast-math

Во-первых, связь между флагами оптимизации LLVM и Rust весьма косвенная. Она ограничивается лишь передачей флагов LLVM через llvm-args. Я уже молчу о неявном изменении порядка вычислений при векторизации и использовании SIMD инструкций. Если на x86 в последнем случае еще всё более-менее предсказуемо, то на ARM, RISC-V и Xtensa можно получить просто массу различных неожиданностей.

Во-вторых, IEEE 754-2019 явно описывает обработку исчезновения порядка. И к Rust моя претензия именно в том, что он не предоставляет адекватного способа паниковать при исчезновении порядка. В CLang я могу явно указать -ffp-exception-behavior=strict. А что мне предлагает Rust?

От того, что у Вас разные результаты ещё не значит, что у Вас уб

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

Во-первых, связь между флагами оптимизации LLVM и Rust весьма косвенная.

Можете воспроизвести на godbolt перестановку инструкций? Потому что насколько мне известно, расту запрещяется переставлять операции с фп, в том числе для векторизации. Собственно именно поэтому в расте сложнее добиться автоматической векторизации в циклах с числами с плавающей запятой.

Если же Вы через -Cllvm-args поменяли это поведение, то Вы знаете что делаете (ССЗБ).

И к Rust моя претензия именно в том, что он не предоставляет адекватного способа паниковать при исчезновении порядка.

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

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

Термин Undefined Behaviour имеет вполне себе конкретное значение - что является или не является уб могут сказать только авторы компилятора, по определению.

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

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

Можете воспроизвести на godbolt перестановку инструкций?

Моего конкретного случая - нет. Там просто отсутствует Xtensa ESP32-S3 для LLVM. GCC в данном случае явно не при чём.

А развлекаться с Cortex-M или RISC-V можете сами, если Вам это интересно.

В таком случае имеет смысл завести пропозал

Ну так очередной итерации уже скоро два года. А воз и ныне там.

Термин Undefined Behaviour имеет вполне себе конкретное значение - что является или не является уб могут сказать только авторы компилятора, по определению.

Так конкретное значение или авторы компилятора могут интерпретировать этот термин по своему усмотрению?

Неопределенное значение выражения IEEE называет UB. Например, для C

int f(int i) {
    // undefined behavior: two unsequenced modifications to i
    return i++ + i++;
}

С какого перепугу неопределенное значение выражения для Rust должно называться иначе? Только потому, что авторы компилятора так захотели? )))

Моего конкретного случая - нет.

Ну так каким образом тогда Ваши слова опровергают исходное утверждение: rustc не переставляет фп операции.

Так конкретное значение или авторы компилятора могут интерпретировать этот термин по своему усмотрению?

Конкретное значение, которое примерно формулируется так: "авторы компилятора* обязаны указать, а что именно является неопределённым поведением — всё остальное им не является".

*на самом деле авторы языка, но в случае раста это суть одно и то же.

С какого перепугу неопределенное значение выражения для Rust должно называться иначе?

Потому что неопределённое поведение означает, что программа нарушает контракт компилятора (и компилятор волен менять её поведение как угодно). А "непредсказуемое значение переменной" означает лишь это — это гораздо более безобидная штука, чем UB. По крайней мере в современной трактовке это работает так.

исходное утверждение: rustc не переставляет фп операции.

При чем тут rustc, если исходное утверждение было об оптимизациях LLVM?

неопределённое поведение означает, что программа нарушает контракт компилятора

С точки зрения IEEE, которое вполне можно считать общепринятым, UB и есть контракт компилятора, что фиксируется в стандарте. Не вижу смысла продолжать дискуссию с человеком, продвигающим семантику UB, радикально отличающуюся от общепринятой и стандартизированной ANSI и IEEE.

О том, что результат операции над float нельзя сравнивать с целым, включая 0, мне рассказывали на лекции в году эдак 2003-ем

и, к сожалению, лгали. Сравнивать float с целыми числами вполне можно. Вот, даже статейку недавно постили неплохую - https://news.ycombinator.com/item?id=47767398

P.S. каюсь, увидел что написано “результат операции”. Но статейка все равно полезная)

Та статья начинается с примера, что

// Outputs 0.89999998
std::cout << std::setprecision(8) << ((0.2f + 0.3f) + 0.4f) << '\n';

// Outputs 0.90000004
std::cout << std::setprecision(8) << (0.2f + (0.3f + 0.4f)) << '\n';

А продолжается о том, что отнюдь не всегда нас это волнует, и вот тут согласен.

Увы, вообще не знаю Rust, но почему-то статья зацепила. Пишу на С и C++. UB при работе с указателями представляется мне дьявольскими кознями, которые придумали, чтобы осложнять мне жизнь. Конечно, перестановка команд и прочие оптимизации до некоторой степени ускоряют работу программ (иногда). Но обычно хочется, чтобы код, который я написал, работал именно так, как я его написал. К примеру, написал "++" - увидел в ассемблере inc в этом месте. Написал пустой цикл - и в этом месте будет задержка, причем ее можно точно посчитать в тактах.

Я довольно часто занимаюсь такими вещами как: загрузить массив, объявить область памяти исполняемой; собрать в памяти некую структуру и использовать ее в качестве таблицы виртуальных функций объекта класса; прочитать данные из файла, подправить немного, и обработать их как массив классов...

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

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

В будущем это наверняка приведет к тому, что компоненты ОС перестанут нормально компилироваться свежими версиями компиляторов, в них будут вылезать неожиданные ошибки, которые при написании кода ошибками не были.

Интересно, а в Rust есть что-то, позволяющее корректно реализовывать низкоуровневые операции?

Но обычно хочется, чтобы код, который я написал, работал именно так, как я его написал. К примеру, написал "++" - увидел в ассемблере inc в этом месте.

Это будет настолько безбожно медленный код, что никому, кроме преподавателей в университете, такой язык не нужен. Даже у С компилятор делает оптимизации, а уже в С++ и подавно.

целый мир низкоуровневых программистов

Очень маленький. А остальные программисты не хотят заморачиваться такими вещами, и очень рады, что умный шайтан машин всё оптимизирует за них.

Rust есть что-то, позволяющее корректно реализовывать низкоуровневые операции?

Там есть аллокаторы, работа с памятью, если скажете, что именно вам нужно, я дам ссылку.

С некоторых пор я перестал понимать, как делать это корректно без UB.

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

Хотите анекдот про качалку в Люберцах? На эту тему уже жаловались в 80-х, ещё до принятия первого стандарта Си. 38 лет назад в Usenet.

Apr 5, 1988
FLAME ON
I’m also sick of people implying that any program that is not strictly conforming is totally non-portable. I write a lot of programs that are portable to a large number of machines but are not portable to ALL machines. I also write a lot of programs that have small, self-contained, sections that handle non-portable things like shared memory allocation, interupt handling, semaphores, etc…

В общем, если стандарт не хочет давать гарантии, тем - иногда - хуже для стандарта. Рецепт в том, чтобы полагаться на поведение своего компилятора и принять, что стандарт может охотно избегать ответов (статус UB позволяет ничего не прояснять)

Живёт идея компилятора (ну, набора опций компилятора), где все недостаточно предсказуемые UB-оптимизации отключены - https://gcc.gnu.org/wiki/boringcc.

Живут барьеры оптимизации в виде asm-вставок, которые "как volatile, только без лишнего доступа к памяти" (фрагмент из Linux, stackoverflow).

Нарушать правило strict aliasing - значит полагаться на UB (стандарт не допускает существования -fno-strict-aliasing), но в операционных системах и браузерах так и делают - отключают, без точечных попыток в may_alias (тоже нестандартный). В документах к комитету при этом признавали, что параллельно диалект -fno-strict-aliasing существует - с ним надо считаться - и говорили, что можно создать ещё диалект -fno-provenance.

Когда язык наслаивают 50 лет, он формирует богатый культурный слой. Скажем, сравнение указателей (через больше/меньше) на разные объекты в Си - это UB, хотя в C++ это лишь Unspecified. Почему... нет, вопрос так даже не стоит, это археологическая находка. В рассылке плюсового комитета однажды проводили раскопки на 100 постов на соседнем культурном слое ([ub] Justification for < not being a total order on pointers?).

Ещё из подзаброшенного черновика

Как рождаются числа

В C++20 malloc и memcpy начали неявно создавать простые (sufficiently trivial) объекты. До C++20 malloc и memcpy создавали UB.

— Но до этого всё работало!

Машины компиляторы постоянно умнеют. Никто не знает, как они смогут проэксплуатировать UB. Люди должны быть на шаг впереди.

В C++23 добавили std::start_lifetime_as, чтобы воспроизвести их магию без UB.

— Но до этого всё работало!

— А теперь может и перестать!

Между двумя взглядами на аллокации всегда было фундаментальное противоречие. Если мы на них посмотрим снизу, со стороны железа (железа без MMU - ещё лучше), то нет ничего, кроме байтов. Если мы на них посмотрим сверху, со стороны стандарта и продвинутых компиляторов, то объекты из байтов возникают только после соблюдения специальных ритуалов. Потом появилось железо со встроенными Capabilities (CHERI), которое проверяет у потолстевших указателей валидность, тип, границы объекта и разрешения, и испортило этот рассказ.

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

Понадеемся же, что std::start_lifetime_as из C++23 будет служить на благо подобных архитектур, а не работать оправданием для странных оптимизаций.

В расте не используется TBAA, поэтому можно взять сырые указатели и использовать ptr::read/ptr::write. Единственное, их не стоит смешивать с ссылками — иначе может быть уб.

Написал пустой цикл - и в этом месте будет задержка, причем ее можно точно посчитать в тактах.

В современных процессорах это имеет мало смысла делать. Слишком много движущихся частей.

Но обычно хочется, чтобы код, который я написал, работал именно так, как я его написал

Внятно написать компилятор под такие требования нельзя. Уб есть необходимое зло. Дальше лишь вопрос в том, как сделать достаточно много уб, чтобы компиляторы хоть как-то могли работать, но при этом чтоьы этим было понятно и удобно пользоваться. В этом аспекте новые языки кмк более удачно подходят.

Иногда странно наблюдать, как С/С++ пытаются заменить на Rust, тогда как С и С++ позиционировались именно как замена ассемблера со всеми его возможностями, а у Rust не было изначально, и нет до сих пор, единой теории, что же такое “безопасное управление памятью” и как это должно быть реализовано в компиляторе.

самое ближайшее где оно реально нужно, это когда мы соприкасаемся с С/С++ апи, например, если нужны обёртки

CreateVertexArrays: unsafe extern "C" fn(i32, *mut u32),

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

я пока лутаю удобство от концепции Раста на поверхности и кайфую без уб на своей тачке, )

ну, если так нужен С/С++ в конце концов есть соседний Зиг и там что-то удобно и что-то тоже прикольно)

это когда мы соприкасаемся с С/С++ апи

это не “С/С++”, а чистый С. Rust не умеет взаимодействовать с С++напрямую, только через прослойку C апи.

Sign up to leave a comment.

Articles