Rust: преобразование чисел с плавающей запятой в целочисленное может привести к неопределенному поведению

Созданный на 31 окт. 2013  ·  234Комментарии  ·  Источник: rust-lang/rust

По состоянию на 18.04.2020

Мы намерены стабилизировать поведение saturating-float-castts для as и стабилизировали небезопасные библиотечные функции, которые обрабатывают предыдущее поведение. См. # 71269 для последней дискуссии о процессе стабилизации.

Статус на 2018-11-05

В компиляторе был реализован флаг -Zsaturating-float-casts , который приведет к тому, что все приведения с плавающей точкой в ​​целочисленные будут иметь «насыщающее» поведение, когда, если оно выходит за пределы, оно ограничивается ближайшей границей. Призыв к сравнительному анализу этого изменения прозвучал некоторое время назад. Результаты, хотя во многих проектах положительные, для некоторых -

Следующие шаги - выяснить, как восстановить производительность в этих случаях:

  • Один из вариантов - взять сегодняшнее поведение as cast (которое в некоторых случаях является UB) и добавить функции unsafe для соответствующих типов и т. Д.
  • Другой - дождаться, пока LLVM добавит концепцию freeze что означает, что мы получим шаблон мусорного бита, но, по крайней мере, не UB
  • Другой - реализовать приведение типов с помощью встроенной сборки в LLVM IR, поскольку текущий кодогенератор не сильно оптимизирован.

Старый статус

ОБНОВЛЕНИЕ (от @nikomatsakis): после долгих обсуждений у нас есть зачатки плана решения этой проблемы. Но нам нужна помощь в фактическом исследовании влияния на производительность и проработке окончательных деталей!


ОРИГИНАЛЬНЫЙ ВЫПУСК СЛЕДУЕТ:

Если значение не может поместиться в ty2, результаты не определены.

1.04E+17 as u8
A-LLVM C-bug I-unsound 💥 P-medium T-lang

Самый полезный комментарий

Я начал некоторую работу по реализации встроенных функций для насыщения типов float до int в LLVM: https://reviews.llvm.org/D54749

Если это произойдет куда-нибудь, это обеспечит относительно малозатратный способ получения насыщающей семантики.

Все 234 Комментарий

Выдвижение

принято для P-старшего, те же рассуждения, что и # 10183

Я не думаю, что это обратно несовместимо на уровне языка. Это не приведет к прекращению работы кода, который работал нормально. Выдвижение.

переход на высокий P, те же рассуждения, что и # 10183

Как мы предлагаем решить эту проблему и # 10185? Поскольку то, определено ли поведение или нет, зависит от динамического значения приводимого числа, кажется, единственным решением является вставка динамических проверок. Мы, кажется, согласны, что не хотим делать это для арифметического переполнения, счастливы ли мы сделать это для переполнения приведения?

Мы могли бы добавить в LLVM встроенную функцию, выполняющую «безопасное преобразование». @zwarich может иметь другие идеи.

AFAIK единственное решение на данный момент - использовать встроенные функции, специфичные для цели. Это то, что делает JavaScriptCore, по крайней мере, по словам кого-то, кого я спросил.

О, тогда это достаточно просто.

ping @pnkfelix покрывается ли это новыми средствами проверки переполнения?

Эти приведения не проверяются rustc с помощью утверждений отладки.

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

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

Это позволяет нарушить безопасность памяти в безопасной ржавчине, пример из этого сообщения на форуме :

Undefs, а? Undef - это весело. Они имеют свойство размножаться. После нескольких минут пререканий ..

#[inline(never)]
pub fn f(ary: &[u8; 5]) -> &[u8] {
    let idx = 1e100f64 as usize;
    &ary[idx..]
}

fn main() {
    println!("{}", f(&[1; 5])[0xdeadbeef]);
}

segfaults в моей системе (последний вечер) с -O.

Маркировка I-unsound с учетом нарушения сохранности памяти при безопасной ржавчине.

@bluss , для меня это не segfualt, просто выдает ошибку утверждения. снятие тегов, так как я был тем, кто добавил это

Вздох, я забыл -O, переназначение тегов.

повторное номинирование на P-high. Очевидно, в какой-то момент это было P-high, но со временем стало ниже. Это кажется очень важным для правильности.

РЕДАКТИРОВАТЬ: не реагировал на комментарий сортировки, добавляя метку вручную.

Похоже, что прецедент из переполнения (например, для переключения) заключается в том, чтобы просто остановиться на каком-то поведении. Кажется, что Java дает результат по модулю диапазона, что не кажется необоснованным; Я не уверен, какой именно код LLVM нам понадобится для этого.

Согласно https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls -5.1.3 Java также гарантирует, что значения NaN отображаются в 0 и бесконечности до минимального / максимального представимого целого числа. Более того, правило Java для преобразования сложнее, чем просто упаковка, это может быть комбинация насыщения (для преобразования в int или long ) и упаковки (для преобразования в меньшие целые типы , если нужно). Воспроизведение всего алгоритма преобразования из Java, безусловно, возможно, но это потребует изрядного количества операций для каждого преобразования. В частности, чтобы гарантировать, что результат операции fpto[us]i в LLVM не демонстрирует неопределенного поведения, потребуется проверка диапазона.

В качестве альтернативы я бы предложил, чтобы приведение типов float-> int гарантированно было действительным только в том случае, если усечение исходного значения может быть представлено как значение целевого типа (или, может быть, как [iu]size ?) И имеют утверждения в отладочных сборках, которые вызывают панику, если значение не было точно представлено.

Основные преимущества подхода Java заключаются в том, что функция преобразования является полной, но это также означает, что может закрасться неожиданное поведение: это предотвратит неопределенное поведение, но было бы легко обмануть, чтобы не проверять, действительно ли приведение имеет смысл (это, к сожалению, верно и для других исполнителей: беспокоится:).

Другой подход соответствует тому, который в настоящее время используется для арифметических операций: простая и эффективная реализация в выпуске, паника, вызванная проверкой диапазона при отладке. К сожалению, в отличие от других приведений as , это приведет к проверке такого преобразования, что может удивить пользователя (хотя, возможно, здесь может помочь аналогия с арифметическими операциями). Это также приведет к поломке некоторого кода, но AFAICT это должно произойти только для кода, который в настоящее время полагается на неопределенное поведение (т.е. он заменит неопределенное поведение "давайте вернем любое целое число, вам, очевидно, все равно, какое" с паникой).

Проблема не в том, что «давайте вернем любое целое число, вам, очевидно, не важно какое», а в том, что это вызывает undef, которое не является случайным значением, а скорее назальным значением демона, и LLVM может предполагать, что undef никогда не возникает. включение оптимизаций, которые делают ужасно неправильные вещи. Если бы это было случайное значение, но, главное, не undef, этого было бы достаточно, чтобы исправить проблемы со звуком. Нам не нужно определять, как представлены непредставимые значения, нам просто нужно предотвратить undef.

Обсуждается на собрании @ rust-lang / compiler. Остается наиболее последовательный план действий:

  1. когда включены проверки переполнения, проверьте наличие незаконных приведений и паники.
  2. в противном случае нам нужно резервное поведение, это должно быть что-то с минимальными (в идеале нулевыми) затратами времени выполнения для допустимых значений, но точное поведение не так важно, если это не LLVM undef.

Основная проблема в том, что нам нужно конкретное предложение по варианту 2.

сортировка: P-средний

@nikomatsakis as когда-нибудь сейчас паникует в отладочных сборках? Если это не так, для единообразия и предсказуемости, кажется, предпочтительнее оставить это так. (Я думаю, что это _должно_ быть_, как и арифметика, но это отдельная и прошедшая дискуссия.)

в противном случае нам нужно резервное поведение, это должно быть что-то с минимальными (в идеале нулевыми) затратами времени выполнения для допустимых значений, но точное поведение не так важно, если это не LLVM undef.

Конкретное предложение: извлеките цифры и показатель степени как u64 и сдвигайте цифры по экспоненте.

fn f64_as_u64(f: f64) -> u64 {
    let (mantissa, exponent, _sign) = f.integer_decode();
    mantissa >> ((-exponent) & 63)
}

Да, это не нулевая цена, но она несколько оптимизируема (лучше, если мы отметим integer_decode inline ) и, по крайней мере, детерминирована. Будущий MIR-проход, который расширяет приведение типа float-> int, вероятно, может проанализировать, будет ли float гарантированно пригоден для приведения и пропустить это тяжелое преобразование.

У LLVM нет встроенных функций платформы для функций преобразования?

РЕДАКТИРОВАТЬ : @zwarich сказал (давным-давно):

AFAIK единственное решение на данный момент - использовать встроенные функции, специфичные для цели. Это то, что делает JavaScriptCore, по крайней мере, по словам кого-то, кого я спросил.

Зачем вообще паниковать? AFAIK , as должен усекать / расширять, _not_ проверять операнды.

В субботу, 5 марта 2016 г., в 03:47:55 -0800 Габор Лехель написал:

@nikomatsakis as когда-нибудь сейчас паникует в отладочных сборках? Если это не так, для единообразия и предсказуемости, кажется, предпочтительнее оставить это так. (Я думаю, что это _должно_ быть_, как и арифметика, но это отдельная и прошедшая дискуссия.)

Правда. Я считаю это убедительным.

В среду, 9 марта 2016 г., в 02:31:05 -0800 Эдуард-Михай Буртеску написал:

У LLVM нет встроенных функций платформы для функций преобразования?

ИЗМЕНИТЬ :

AFAIK единственное решение на данный момент - использовать встроенные функции, специфичные для цели. Это то, что делает JavaScriptCore, по крайней мере, по словам кого-то, кого я спросил.

Зачем вообще паниковать? AFAIK , as должен усекать / расширять, _not_ проверять операнды.

Да, я думаю, что раньше ошибался. as - это "усечение без проверки"
оператор, к лучшему или худшему, и кажется, что лучше оставаться последовательным
с этой философией. Использование встроенных функций для конкретной цели может быть идеальным
прекрасное решение?

@nikomatsakis : кажется, поведение еще не определено? Не могли бы вы рассказать о планах на этот счет?

Просто столкнулся с этим с гораздо меньшими числами

    let x: f64 = -1.0;
    x as u8

Результаты в 0, 16 и т.д. в зависимости от оптимизации, я надеялся, что он будет определен как 255, поэтому мне не нужно писать x as i16 as u8 .

@gmorenz Вы пробовали !0u8 ?

В контексте, который не имел бы смысла, я получал f64 от преобразования данных, отправляемых по сети, с диапазоном [-255, 255]. Я надеялся, что это будет красиво (точно так же, как <i32> as u8 ).

Вот недавнее предложение LLVM «убить undef» http://lists.llvm.org/pipermail/llvm-dev/2016-October/106182.html , хотя я не достаточно осведомлен, чтобы знать, разрешит ли это автоматически Эта проблема.

Они заменяют undef на яд, семантика немного отличается. Это не приведет к тому, что int -> float приведет к определенному поведению.

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

Похоже, это следует пометить как I-crash, учитывая https://github.com/rust-lang/rust/issues/10184#issuecomment -139858153.

У нас был вопрос об этом сегодня в #rust-beginners , кто-то наткнулся на это в дикой природе.

В книге, которую я пишу с @jimblandy , _Programming Rust_, упоминается эта ошибка.

Разрешены несколько видов повязок.

  • Числа могут быть преобразованы из любого встроенного числового типа в любой другой.

    (...)

    Однако на момент написания этой статьи приведение большого значения с плавающей запятой к целочисленному типу, который слишком мал для его представления, может привести к неопределенному поведению. Это может вызвать сбои даже в безопасном Rust. Это ошибка компилятора github.com/rust-lang/rust/issues/10184 .

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

Судя по всему, текущий JavaScriptCore использует интересный прием на x86. Они используют инструкцию CVTTSD2SI, а затем возвращаются к небрежному C ++, если значение выходит за пределы допустимого диапазона. Поскольку в настоящее время значения вне диапазона резко увеличиваются, использование этой инструкции (без отката!) Было бы улучшением того, что у нас есть сейчас, хотя и только для одной архитектуры.

Честно говоря, я думаю, что мы должны отказаться от числовых приведений с помощью as и использовать вместо них From и TryFrom или что-то вроде conv crate.

Может быть, но мне это кажется ортогональным.

Хорошо, я только что перечитал весь этот разговор. Я думаю, есть согласие, что эта операция не должна вызывать панику (для общей согласованности с as ). Есть два основных претендента на то, каким должно быть поведение:

  • Какой-то определенный результат

    • Pro: Я думаю, что это максимально соответствует нашей общей философии до сих пор.

    • Против: По-видимому, нет действительно портативного способа получения какого-либо конкретного определенного результата в этом случае. Это означает, что мы будем использовать встроенные функции для конкретной платформы с каким-то запасным вариантом для значений, выходящих за пределы диапазона (например, возврат к насыщению , эта функция, предложенная @ oli-obk , определение Java или любой другой "волосатый C ++" АО использует .

    • В худшем случае, мы можем просто вставить некоторые if для случаев, когда "вне диапазона".

  • Неопределенное значение (не неопределенное поведение)

    • Pro: это позволяет нам просто использовать встроенные функции для конкретной платформы, доступные на каждой платформе.

    • Против: это опасность переносимости. В общем, мне кажется, что мы не очень часто использовали неопределенные результаты, по крайней мере, на языке (я уверен, что мы это делаем в библиотеках в разных местах).

Мне непонятно, есть ли четкий прецедент, каким должен быть результат в первом случае?

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

Мне нравится насыщенность, потому что я могу ее понять, и она кажется полезной, но кажется какой-то несовместимой с тем, как u64 as u32 выполняет усечение. Так что, возможно, какой-то результат, основанный на усечении, имеет смысл, что, я думаю, вероятно, было предложено @ oli-obk - я не совсем понимаю, для чего предназначен этот код. знак равно

Мой код дает правильное значение для вещей в диапазоне 0..2 ^ 64 и детерминированные, но фиктивные значения для всего остального.

числа с плавающей запятой представлены мантиссой ^ экспонента, например, 1.0 - это (2 << 52) ^ -52 и поскольку битовые сдвиги и экспоненты в двоичном формате - одно и то же, мы можем просто отменить сдвиг (таким образом, отрицание экспоненты и правого сдвиг).

+1 за детерминизм.

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

  • Насыщенность (значения вне диапазона становятся IntType::max_value() / min_value() )
  • По модулю (значения, выходящие за пределы допустимого диапазона, обрабатываются так, как будто сначала они преобразуются в bigint, а затем усекаются)

Приведенная ниже таблица предназначена для полного определения обоих вариантов. T - это любой машинный целочисленный тип. Tmin и Tmax равны T::min_value() и T::max_value() . RTZ (v) означает математическое значение v и округление до нуля, чтобы получить математическое целое число.

v | v as T (насыщенность) | v as T (по модулю)
---- | ---- | ----
в диапазоне (Tmin <= v <= Tmax) | РТЗ (в) | РТЗ (в)
отрицательный ноль | 0 | 0
NaN | 0 | 0
Бесконечность | Tmax | 0
-Бесконечность | Tmin | 0
v> Tmax | Tmax | RTZ (v) усечено, чтобы соответствовать T
v <Tmin | Tmin | RTZ (v) усечено, чтобы соответствовать T

Стандарт ECMAScript определяет операции ToInt32 , ToUint32, ToInt16, ToUint16, ToInt8, ToUint8, и мое намерение с опцией «modulo» выше состоит в том, чтобы соответствовать этим операциям в каждом случае.

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

Предложение @oli-obk - это третий способ, который стоит рассмотреть, если он быстрее вычисляется для значений, находящихся в диапазоне.

@ oli-obk А как насчет целочисленных типов со знаком?

Добавим еще одно предложение: пометьте преобразование u128 в float как небезопасное и заставьте людей явно выбирать способ обработки. u128 в настоящее время встречается довольно редко.

@Manishearth Я бы надеялся на подобную семантику целых чисел → числа с плавающей точкой как числа с плавающей точкой → целые числа. Поскольку оба являются UB-полными, и мы больше не можем сделать небезопасным float → integer, нам, вероятно, также следует избегать небезопасного integer → float.

Для float → целочисленное насыщение будет быстрее AFAICT (в результате получается последовательность and , проверка + прыжок, сравнение с плавающей точкой и прыжок, все для 0,66 или 0,5 2-3 цикла на современных дугах). Лично мне наплевать на то, какое именно поведение мы выберем, пока значения в диапазоне являются настолько быстрыми, насколько это возможно.

Разве не было бы смысла заставить его вести себя как переполнение? Так что в отладочной сборке может возникнуть паника, если вы выполните приведение с неопределенным поведением. Тогда у вас могут быть методы для определения поведения приведения, такие как 1.04E+17.saturating_cast::<u8>() , unsafe { 1.04E+17.unsafe_cast::<u8>() } и, возможно, другие.

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

@cryze UB не должен существовать даже в режиме выпуска в безопасном коде. Материал переполнения по-прежнему определяется поведением.

Тем не менее, паника при отладке ипо релизу было бы здорово.

Это влияет на:

  • f32 -> u8, u16, u32, u64, u128, usize ( -1f32 as _ для всех, f32::MAX as _ для всех, кроме u128)
  • f32 -> i8, i16, i32, i64, i128, isize ( f32::MAX as _ для всех)
  • f64 -> все целые ( f64::MAX as _ для всех)

f32::INFINITY as u128 также является UB

@CryZe

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

Это то, что я думал изначально, но мне напомнили, что as конверсии в настоящее время не вызывают паники (мы не выполняем проверку переполнения с помощью as , хорошо это или плохо). Таким образом, наиболее аналогично, когда он «делает что-то определенное».

FWIW, вещь "kill undef", на самом деле, предоставит способ исправить небезопасность памяти, но оставит результат недетерминированным. Один из ключевых компонентов:

3) Создайте новую инструкцию '% y = freeze% x', которая останавливает распространение
яд. Если ввод является ядом, он возвращает произвольный, но фиксированный,
значение. (как старый undef, но каждое использование получает одно и то
просто возвращает свое входное значение.

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

... Но не детерминировано для разных архитектур, если кому-то интересно. x86 возвращает 0x80000000 для всех неверных вводов; ARM насыщается для входов, выходящих за пределы допустимого диапазона, и (если я читаю этот псевдокод правильно) возвращает 0 для NaN. Так что, если цель состоит в том, чтобы получить детерминированный и независимый от платформы результат , недостаточно просто использовать встроенную функцию fp-to-int; по крайней мере, на ARM вам также необходимо проверить регистр статуса на предмет исключения. Это может иметь некоторые накладные расходы само по себе и, безусловно, предотвращает автовекторизацию в том маловероятном случае, когда использование встроенной функции еще не было. В качестве альтернативы, я думаю, вы можете явно проверить значения в диапазоне, используя обычные операции сравнения, а затем использовать обычное преобразование float в int. Оптимизатор звучит намного лучше…

as конверсии в настоящее время не вызывают паники

В какой-то момент мы изменили + на панику (в режиме отладки). Я не был бы шокирован, если бы увидел панику as в случаях, которые раньше были UB.

Если мы заботимся о проверке (что нам следует), то мы должны либо отказаться от as (есть ли какой-либо вариант использования, в котором это единственный хороший вариант?), Либо, по крайней мере, посоветовать не использовать его и переместить людей на такие вещи, как Вместо этого TryFrom и TryInto , это то, что мы сказали, что планируем сделать, когда было решено оставить as как есть. Я не чувствую, что обсуждаемые случаи качественно, абстрактно , от тех случаев, когда as уже определено, чтобы не проводить никаких проверок. Разница лишь в том, что на практике реализация для этих случаев на данный момент не завершена и имеет UB. Мир, в котором вы не можете полагаться на as выполняющие проверки (потому что для большинства типов это не так), и вы не можете полагаться на то, что он не паникует (потому что для некоторых типов это будет), и это непоследовательно, и мы до сих пор не отказались от него, мне он кажется худшим из всех.

Итак, я думаю, что на этом этапе @jorendorff в основном перечислил то, что мне кажется лучшим планом :

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

Он перечислил три возможности. Я думаю, что осталась работа над исследованием этих возможностей - или, по крайней мере, исследованием одной из них. То есть на самом деле реализуйте это и попытайтесь почувствовать, насколько он «медленный» или «быстрый».

Есть ли кто-нибудь, кто чувствует себя мотивированным, чтобы нанести удар по этому поводу? Я собираюсь отметить это как E-help-wanted в надежде привлечь кого-нибудь. (@ oli-obk?)

Ух, я бы предпочел не платить за кроссплатформенную согласованность: / Это мусор, мне все равно, какой мусор выйдет (однако отладочное утверждение было бы очень полезно).

В настоящее время все функции округления / усечения в Rust очень медленные (вызовы функций с кропотливо точными реализациями), поэтому as - это мой последний способ быстрого округления с плавающей запятой.

Если вы собираетесь сделать as чем-то большим, чем просто cvttss2si , пожалуйста, также добавьте стабильную альтернативу.

@pornel, это не просто UB теоретического типа, где все в порядке, если вы игнорируете, что это ub, это имеет последствия для реального мира. Я извлек # 41799 из реального примера кода.

@ est31 Я согласен с тем, что оставлять его как UB неправильно, но я видел freeze предложенный в качестве решения для UB. AFAIK, который делает его определенным детерминированным значением, вы просто не можете сказать, какое именно. Такое поведение меня устраивает.

Так что было бы нормально, если бы, например, u128::MAX as f32 детерминированно создавал 17.5 на x86, 999.0 на x86-64 и -555 на ARM.

freeze не даст определенного, детерминированного, неопределенного значения. Его результатом по-прежнему является «любой битовый шаблон, который нравится компилятору», и он согласован только при использовании одной и той же операции. Это может обойти примеры создания UB, которые люди собрали выше, но не даст этого:

u128 :: MAX, поскольку f32 детерминированно производил 17,5 на x86, 999,0 на x86-64 и -555 на ARM.

Например, если LLVM замечает, что u128::MAX as f32 переполняется, и заменяет его на freeze poison , допустимое снижение fn foo() -> f32 { u128::MAX as f32 } на x86_64 может быть таким:

foo:
  ret

(то есть просто вернуть то, что было последним сохранено в регистре возврата)

Понимаю. Это все еще приемлемо для моего использования (для случаев, когда я ожидаю, что значения выходят за пределы диапазона, я заранее фиксирую значения. Если я ожидаю значения в диапазоне, но это не так, тогда я не получу правильный результат, несмотря ни на что) .

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

Доступно ли что-то вроде freeze на LLVM? Я думал, что это чисто теоретическая конструкция.

@nikomatsakis Я никогда не видел, чтобы он использовался таким образом (в отличие от poison ) - это плановая модернизация отравы / undef.

freeze вообще не существует в LLVM. Это только было предложено ( этот документ PLDI представляет собой автономную версию, но также много обсуждался в списке рассылки). Предложение, похоже, получило значительную поддержку, но, конечно, это не гарантия того, что оно будет принято, не говоря уже о своевременном принятии. (Удаление типов указателей из типов указателей было принято в течение многих лет, но до сих пор не сделано.)

Хотим ли мы открыть RFC для более широкого обсуждения предлагаемых здесь изменений? ИМО, все, что потенциально может повлиять на производительность as , будет спорным, но это будет вдвойне спорным, если мы не дадим людям возможность сделать так, чтобы их голос был услышан.

Я разработчик Julia и какое-то время слежу за этой проблемой, так как мы используем один и тот же бэкэнд LLVM и поэтому имеем аналогичные проблемы. Если это интересно, вот что мы выбрали (с примерным временем выполнения одной функции на моей машине):

  • unsafe_trunc(Int64, x) сопоставляется напрямую с соответствующей внутренней функцией LLVM fptosi (1,5 нс)
  • trunc(Int64, x) выдает исключение для значений вне диапазона (3 нс)
  • convert(Int64, x) выдает исключение для значений вне диапазона или нецелых значений (6 нс)

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

@bstrie Меня устраивает RFC, но я думаю, что было бы полезно иметь данные! Однако комментарий

Я поигрался с семантикой JS (упомянутый модуль @jorendorff ) и семантикой Java, которая выглядит как столбец «насыщенность». Если срок действия этих ссылок истекает, это JS и Java .

Я также разработал быструю реализацию насыщения в Rust, что, на мой взгляд (?), Правильно. И также получил некоторые контрольные цифры . Интересно, что я вижу, что насыщающая реализация в 2-3 раза медленнее, чем внутренняя, что отличается от того, что обнаружил @simonbyrne, только в 2 раза медленнее.

Я не совсем уверен, как реализовать семантику "мода" в Rust ...

Мне, однако, кажется очевидным, что нам понадобится множество методов f32::as_u32_unchecked() и тому подобное для тех, кому нужна производительность.

кажется очевидным, что нам понадобится множество методов f32::as_u32_unchecked() и тому подобное для тех, кому нужна производительность.

Это облом - или вы имеете в виду безопасный, но определяемый реализацией вариант?

Нет ли возможности для быстрой реализации по умолчанию?

@eddyb Я думал, что у нас будет просто unsafe fn as_u32_unchecked(self) -> u32 на f32 и это прямой аналог того, что as сегодня.

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

То есть дешевого платформенно-зависимого варианта нет? Я хочу что-то быстрое, с неопределенным значением за пределами допустимого диапазона и безопасное. Мне не нужен UB для некоторых входных данных, и я думаю, что это слишком опасно для обычного использования, если мы сможем сделать лучше.

Насколько мне известно, на большинстве, если не на всех платформах канонический способ реализации этого преобразования что- то делает с входными данными вне диапазона, что не является UB. Но у LLVM, похоже, нет способа выбрать этот вариант (каким бы он ни был) вместо UB. Если бы мы могли убедить разработчиков LLVM ввести встроенную функцию, которая дает результат «неуказанный, но не undef / poison » для входных данных вне допустимого диапазона, мы могли бы это использовать.

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

На всякий случай вы будете выбирать между этими:

Насыщенность (значения вне диапазона становятся IntType :: max_value () / min_value ())
По модулю (значения, выходящие за пределы допустимого диапазона, обрабатываются так, как будто сначала они преобразуются в bigint, а затем усекаются)

Здесь имеет смысл только насыщение IMO, потому что абсолютная точность с плавающей запятой быстро падает по мере увеличения значений, поэтому в какой-то момент модуль будет чем-то бесполезным, как все нули.

Я пометил это как E-needs-mentor и пометил его тегом WG-compiler-middle так как кажется, что период имплантации может быть прекрасным временем для дальнейшего исследования! Мои существующие примечания к плану записи довольно скудны , поэтому было бы здорово, если бы кто-нибудь из @ rust-lang / compiler захотел помочь разработать их немного дальше!

@nikomatsakis

IIRC LLVM планируют в конечном итоге реализовать freeze , что должно позволить нам иметь дело с UB путем выполнения freeze .

Мои результаты на данный момент: https://gist.github.com/s3bk/4bdfbe2acca30fcf587006ebb4811744

Варианты _array запускают цикл из 1024 значений.
_cast: x as i32
_clip: x.min (MAX) .max (MIN) как i32
_panic: паника, если x выходит за пределы
_zero: устанавливает нулевой результат, если он выходит за границы

test bench_array_cast       ... bench:       1,840 ns/iter (+/- 37)
test bench_array_cast_clip  ... bench:       2,657 ns/iter (+/- 13)
test bench_array_cast_panic ... bench:       2,397 ns/iter (+/- 20)
test bench_array_cast_zero  ... bench:       2,671 ns/iter (+/- 19)
test bench_cast             ... bench:           2 ns/iter (+/- 0)
test bench_cast_clip        ... bench:           2 ns/iter (+/- 0)
test bench_cast_panic       ... bench:           2 ns/iter (+/- 0)
test bench_cast_zero        ... bench:           2 ns/iter (+/- 0)

Возможно, вам не нужно округлять результаты до целого числа для отдельных операций. Очевидно, что за этими 2 нс / л должна быть какая-то разница. Или это действительно так, ровно 2 нс на все 4 варианта?

@ sp-1234 Интересно, он частично оптимизирован.

@ sp-1234 Это слишком быстро для измерения. Тесты без массивов в основном бесполезны.
Если вы сделаете однозначные функции функциями через #[inline(never)] , вы получите 2 нс против 3 нс.

@ arielb1
У меня есть некоторые оговорки относительно freeze . Если я правильно понимаю, замороженный undef все еще может содержать любое произвольное значение, оно просто не будет меняться между использованиями. На практике компилятор, вероятно, будет повторно использовать регистр или слот стека.

Однако это означает, что теперь мы можем читать неинициализированную память из безопасного кода. Это могло привести к утечке секретных данных, что-то вроде Heartbleed. Спорный вопрос, действительно ли это считается UB с точки зрения Rust, но это явно кажется нежелательным.

Я запустил тест @ s3bk локально. Я могу подтвердить, что скалярные версии полностью оптимизированы, и asm для вариантов массива также выглядит подозрительно хорошо оптимизированным: например, циклы векторизованы, что хорошо, но затрудняет экстраполяцию производительности на скалярный код.

К сожалению, рассылка спама black_box , похоже, не помогает. Я действительно вижу, что asm выполняет полезную работу, но запуск теста по-прежнему дает 0 нс для скалярных тестов (кроме cast_zero , который показывает 1 нс). Я вижу, что @alexcrichton 100 раз выполнила сравнение в своих тестах, поэтому я применил тот же метод. Сейчас я вижу эти числа ( исходный код ):

test bench_cast             ... bench:          53 ns/iter (+/- 0)
test bench_cast_clip        ... bench:         164 ns/iter (+/- 1)
test bench_cast_panic       ... bench:         172 ns/iter (+/- 2)
test bench_cast_zero        ... bench:         100 ns/iter (+/- 0)

Тесты массивов слишком сильно различаются, чтобы я мог им доверять. Что ж, по правде говоря, я все равно скептически отношусь к инфраструктуре бенчмаркинга test , особенно после того, как увидел вышеупомянутые цифры по сравнению с плоскими 0ns, которые я получил ранее. Более того, даже 100 итераций black_box(x); (в качестве базового показателя) занимают 34 нс, что еще больше затрудняет надежную интерпретацию этих чисел.

Следует отметить два момента:

  • Несмотря на то, что NaN не обрабатывается специально (он возвращает -inf вместо 0?), Реализация cast_clip оказывается медленнее, чем насыщающий приведение @alexcrichton (обратите внимание, что их запуск и мой имеют примерно одинаковое время для as приведения, 53-54нс).
  • В отличие от результатов массива @ s3bk , я вижу, что cast_panic медленнее, чем другие проверенные приведения. Я также вижу еще большее замедление в тестах массивов. Может быть, эти вещи просто сильно зависят от деталей микроархитектуры и / или настроения оптимизатора?

Для справки, я измерял с помощью rustc 1.21.0-nightly (d692a91fa 2017-08-04), -C opt-level=3 , на i7-6700K при небольшой нагрузке.


В заключение я прихожу к выводу, что до сих пор нет надежных данных и что получение более надежных данных кажется трудным. Более того, я сильно сомневаюсь, что какое-либо реальное приложение тратит на эту операцию хотя бы 1% времени своих настенных часов. Поэтому я бы посоветовал двигаться дальше, реализовав насыщающие приведения as в rustc , за флагом -Z , а затем запустив несколько неискусственных тестов с этим флагом и без него, чтобы определить влияние на реалистичные Приложения.

Изменить: я бы также рекомендовал запускать такие тесты на различных архитектурах (например, включая ARM) и микроархитектурах, если это вообще возможно.

Признаюсь, я не так хорошо знаком с ржавчиной, но я думаю, что эта строка немного неверна: std::i32::MAX (2 ^ 31-1) не совсем представима как Float32, поэтому std::i32::MAX as f32 будет округлено до ближайшего представимого значения (2 ^ 31). Если это значение используется в качестве аргумента x , результат технически не определен. Замена на строгое неравенство должна исправить этот случай.

Да, у нас раньше была именно такая проблема в Servo. Окончательным решением было забросить на f64 и потом зажать.

Есть и другие решения, но они довольно хитрые, и ржавчина не предоставляет хороших API для работы с этим.

использование 0x7FFF_FF80i32 в качестве верхнего предела и -0x8000_0000i32 должно решить эту проблему без преобразования в f64.
изменить: используйте правильное значение.

Я думаю, вы имеете в виду 0x7fff_ff80 , но простое использование строгого неравенства, вероятно, сделало бы намерение кода более ясным.

как в x < 0x8000_0000u32 as f32 ? Вероятно, это было бы хорошей идеей.

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

Меня немного беспокоит предлагаемая реализация, потому что она неправильно транслируется в машинные инструкции и сильно зависит от ветвления. Ветвление делает производительность зависимой от конкретных шаблонов данных. В приведенных выше тестовых примерах все выглядит (сравнительно) быстро, потому что всегда выполняется одна и та же ветвь, а процессор имеет хорошие данные прогнозирования ветвлений из многих предыдущих итераций цикла. Реальный мир, вероятно, так не будет выглядеть. Кроме того, ветвление ухудшает способность компилятора векторизовать код. Я не согласен с мнением @rkruppe , что операцию также не следует тестировать в сочетании с векторизацией. Векторизация важна для высокопроизводительного кода, и возможность векторизации простых приведений на общих архитектурах должна быть решающим требованием.

По причинам, указанным выше, я поигрался с альтернативной версией исправлением @simonbyrne . Я реализовал его для u16 , i16 и i32, поскольку все они должны охватывать несколько разные случаи, что приводит к разной производительности.

Результаты:

test i16_bench_array_cast       ... bench:          99 ns/iter (+/- 2)
test i16_bench_array_cast_clip  ... bench:         197 ns/iter (+/- 3)
test i16_bench_array_cast_clip2 ... bench:         113 ns/iter (+/- 3)
test i16_bench_cast             ... bench:          76 ns/iter (+/- 1)
test i16_bench_cast_clip        ... bench:         218 ns/iter (+/- 25)
test i16_bench_cast_clip2       ... bench:         148 ns/iter (+/- 4)
test i16_bench_rng_cast         ... bench:       1,181 ns/iter (+/- 17)
test i16_bench_rng_cast_clip    ... bench:       1,952 ns/iter (+/- 27)
test i16_bench_rng_cast_clip2   ... bench:       1,287 ns/iter (+/- 19)

test i32_bench_array_cast       ... bench:         114 ns/iter (+/- 1)
test i32_bench_array_cast_clip  ... bench:         200 ns/iter (+/- 3)
test i32_bench_array_cast_clip2 ... bench:         128 ns/iter (+/- 3)
test i32_bench_cast             ... bench:          74 ns/iter (+/- 1)
test i32_bench_cast_clip        ... bench:         168 ns/iter (+/- 3)
test i32_bench_cast_clip2       ... bench:         189 ns/iter (+/- 3)
test i32_bench_rng_cast         ... bench:       1,184 ns/iter (+/- 13)
test i32_bench_rng_cast_clip    ... bench:       2,398 ns/iter (+/- 41)
test i32_bench_rng_cast_clip2   ... bench:       1,349 ns/iter (+/- 19)

test u16_bench_array_cast       ... bench:          99 ns/iter (+/- 1)
test u16_bench_array_cast_clip  ... bench:         136 ns/iter (+/- 3)
test u16_bench_array_cast_clip2 ... bench:         105 ns/iter (+/- 3)
test u16_bench_cast             ... bench:          76 ns/iter (+/- 2)
test u16_bench_cast_clip        ... bench:         184 ns/iter (+/- 7)
test u16_bench_cast_clip2       ... bench:         110 ns/iter (+/- 0)
test u16_bench_rng_cast         ... bench:       1,178 ns/iter (+/- 22)
test u16_bench_rng_cast_clip    ... bench:       1,336 ns/iter (+/- 26)
test u16_bench_rng_cast_clip2   ... bench:       1,207 ns/iter (+/- 21)

Тест проводился на процессоре Intel Haswell i5-4570 и Rust 1.22.0-nightly.
clip2 - это новая реализация без ответвлений. Он согласуется с clip на всех 2 ^ 32 возможных входных значениях f32.

Для тестов rng используются случайные входные значения, которые часто встречаются в разных случаях. Это раскрывает _extreme_ затраты на производительность (примерно в 10 раз превышающие обычные затраты !!!), которые возникают в случае сбоя предсказания ветвления. Я думаю, что это очень важно учитывать. Это тоже не средняя реальная производительность, но это все же возможный случай, и некоторые приложения будут соответствовать этому. Люди ожидают, что приведение f32 будет иметь стабильную производительность.

Сравнение сборки на x86: https://godbolt.org/g/AhdF71
Версия без ответвлений очень хорошо соответствует инструкциям minss / maxss.

К сожалению, мне не удалось заставить Godbolt генерировать сборку ARM из Rust, но вот сравнение методов ARM с Clang: https://godbolt.org/g/s7ronw
Не имея возможности тестировать код и хорошо разбираясь в ARM: размер кода тоже кажется меньше, и LLVM в основном генерирует vmax / vmin, что выглядит многообещающим. Может быть, со временем можно научить LLVM сворачивать большую часть кода в одну инструкцию?

@ActuallyaDeviloper asm и результаты тестов выглядят очень хорошо! Кроме того, безветвленный код, подобный вашему, вероятно, легче сгенерировать в rustc чем вложенные условные выражения других решений (для записи, я предполагаю, что мы хотим сгенерировать встроенный IR вместо вызова функции элемента lang). Большое спасибо за то, что написали это.

У меня вопрос о u16_cast_clip2 : похоже, он не обрабатывает NaN ?! Есть комментарий о NaN, но я считаю, что функция передаст NaN через немодифицированный и попытается преобразовать его в f32 (и даже если бы этого не произошло, это дало бы одно из граничных значений вместо 0 ).

PS: Для ясности, я не пытался намекнуть, что неважно, можно ли векторизовать приведение. Очевидно, что это важно, если окружающий код можно векторизовать. Но скалярная производительность также важна, поскольку векторизация часто неприменима, и тесты, которые я комментировал, не делали никаких заявлений о скалярной производительности. Ради интереса, проверили ли вы asm тестов *array* чтобы увидеть, все ли они векторизованы с вашей реализацией?

@rkruppe Вы правы, я случайно поменял местами if и забыл об этом. f32 as u16 поступил правильно, усек верхний 0x8000, так что тесты его тоже не поймали. Я исправил проблему, снова поменяв ветки местами и на этот раз протестировав все методы с помощью if (y.is_nan()) { panic!("NaN"); } .

Я обновил свой предыдущий пост. Код x86 вообще не претерпел значительных изменений, но, к сожалению, это изменение по какой-то причине не позволяет LLVM генерировать vmax в случае u16 ARM. Я предполагаю, что это связано с некоторыми подробностями обработки NaN этой инструкции ARM или, возможно, это ограничение LLVM.

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

Версии массива векторизованы.
Godbolt: https://godbolt.org/g/HnmsSV

Re: ARM asm , я считаю, что причина, по которой vmax больше не используется, заключается в том, что он возвращает NaN, если любой из операндов равен NaN . Тем не менее, код по-прежнему не имеет ветвей, он просто использует предикативные ходы ( vmovgt , ссылаясь на результат предыдущего vcmp с 0).

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

Ооо, верно. Ницца.

Я бы посоветовал двигаться дальше, реализовав насыщение как приведение в rustc, за флагом -Z

Я реализовал это и запишу PR, как только исправлю # 41799 и проведу еще много тестов.

45134 указал путь кода, который я пропустил (создание константных выражений LLVM - это отдельно от собственной оценки констант rustc). Я добавлю исправление для этого в тот же PR, но это займет немного больше времени.

@rkruppe Вы должны скоординировать свои действия с @ oli-obk, чтобы miri внесла такие же изменения.

Запрос на вытягивание завершен: # 45205

45205 был объединен, так что теперь любой может (ну, начиная со следующего вечера) измерить влияние насыщения на производительность, передав -Z saturating-float-casts через RUSTFLAGS . [1] Такие измерения были бы очень ценными для принятия решения о том, как действовать дальше.

[1] Строго говоря, это не повлияет на неуниверсальные, не #[inline] части стандартной библиотеки, поэтому для 100% точности вы захотите локально построить std с помощью Xargo. Однако я не ожидаю, что это повлияет на большой объем кода (например, различные признаки преобразования: #[inline] ).

@rkruppe Я предлагаю создать страницу внутренних https://internals.rust-lang.org/t/help-us-benchmark-incremental-compilation/6153/ (тогда мы также можем свяжите людей с этим, а не с какими-то случайными комментариями в нашем трекере проблем)

@rkruppe, вам следует создать проблему отслеживания. Это обсуждение уже разбито на два вопроса. Это не хорошо!

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

@ est31 Хм. Хотя флаг -Z охватывает оба направления произнесения (что могло быть ошибкой, если оглянуться назад), кажется маловероятным, что мы включим оба переключателя одновременно, и между ними мало общего с точки зрения того, что должно можно обсудить (например, этот вопрос зависит от производительности насыщения, а в # 41799 согласовано, какое решение является правильным).
Это немного глупо , что тесты в первую очередь ориентированы на этом вопросе будет также оценить влияние исправления на # 41799, но это может в большинстве приводит к завышению регрессий производительности, так что я вроде в порядке с этим. (Но если у кого-то есть мотивация разделить флаг -Z на два, продолжайте.)

Я рассмотрел проблему отслеживания для задачи удаления флага после того, как он изжил себя, но я не вижу необходимости объединять обсуждения, происходящие здесь и в # 41799.

Я подготовил внутреннюю запись: https://gist.github.com/Gankro/feab9fb0c42881984caf93c7ad494ebd

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

Еще один лакомый кусочек заключается в том, что стоимость преобразования float-> int зависит от текущей реализации, а не является фундаментальной. На x86 cvtss2si cvttss2si возвращает 0x80000000 в случаях слишком низкого, слишком высокого и nan, поэтому можно реализовать -Zsaturating-float-casts с помощью cvtss2si cvttss2si за которым следует специальный код в случае 0x80000000, так что в общем случае это может быть только одна ветвь сравнения и предсказания. В ARM vcvt.s32.f32 имеет семантику -Zsaturating-float-casts . LLVM в настоящее время не оптимизирует дополнительные проверки ни в том, ни в другом случае.

@Gankro

Отлично, спасибо большое! Я оставил несколько заметок по сути. Прочитав это, я хотел бы попытаться отделить приведение u128-> f32 от флага -Z. Просто ради избавления от отвлекающей оговорки о том, что флаг закрывает две ортогональные детали.

(Я подал # 45900, чтобы перефокусировать флаг -Z, чтобы он охватил только проблему float-> int)

Было бы неплохо, если бы мы могли получить реализации для конкретных платформ в стиле @sunfishcode (по крайней мере, для x86), прежде чем запрашивать массовое тестирование. Это не должно быть очень сложно.

Проблема в том, что LLVM в настоящее время не предоставляет способа сделать это, насколько мне известно, за исключением, возможно, встроенного asm, который я бы не обязательно рекомендовал для выпуска.

Я обновил черновик, чтобы отразить обсуждение (в основном вырывая любое встроенное упоминание u128 -> f32 в дополнительный раздел в конце).

@sunfishcode Вы уверены? Разве llvm.x86.sse.cvttss2si тем, что вы ищете?

Вот ссылка на игровую площадку, которая его использует:

https://play.rust-lang.org/?gist=33cf9e0871df2eb2475b845af4f1b574&version=nightly

В режиме выпуска оба float_to_int_with_intrinsic и float_to_int_with_as компилируются в одну инструкцию. (В режиме отладки float_to_int_with_intrinsic тратит впустую несколько инструкций, устанавливая ноль в максимум, но это не так уж плохо.)

Кажется, даже постоянное сворачивание выполняется правильно. Например,

float_to_int_with_intrinsic(42.0)

становится

movl    $42, %eax

Но значение вне допустимого диапазона,

float_to_int_with_intrinsic(42.0e33)

не сворачивается:

cvttss2si   .LCPI2_0(%rip), %eax

(В идеале он должен сбрасываться до константы 0x80000000, но в этом нет ничего страшного. Важно то, что он не производит undef.)

О, круто. Похоже, это сработает!

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

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

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

Наконец, я даже не уверен, что построение на cvttss2si будет быстрее, чем то, что у нас есть сейчас (хотя в ARM явно лучше просто использовать соответствующую инструкцию):

  • Вам нужно сравнение, чтобы заметить, что преобразование возвращает 0x80000000, и в этом случае вам все равно нужно другое сравнение (входного значения), чтобы узнать, следует ли возвращать int :: MIN или int :: MAX. И если это целочисленный тип со знаком, я не понимаю, как избежать третьего сравнения, чтобы отличить NaN. Итак, в худшем случае:

    • вы не экономите на количестве сравнений / выборок

    • вы торгуете сравнением с плавающей запятой для сравнения int, что может быть хорошо для ядер OoO (если у вас узкое место в FU, которые могут выполнять сравнения, что кажется относительно большим, если), но это сравнение также зависит от числа с плавающей запятой -> int сравнение, в то время как сравнения в текущей реализации независимы, поэтому далеко не очевидно, что это победа.

  • Вероятно, векторизация станет более трудной или невозможной. Я не ожидаю, что векторизатор цикла вообще справится с этой внутренней функцией.
  • Также стоит отметить, что (AFAIK) эта стратегия применима только к некоторым целочисленным типам. Например, f32 -> u8 потребует дополнительных корректировок результата, что делает эту стратегию явно невыгодной. Я не совсем уверен, на какие типы это влияет (например, я не знаю, есть ли инструкция для f32 -> u32), но приложение, использующее только эти типы, не получит никакого преимущества.
  • Вы можете сделать решение ветвления только с одним сравнением на счастливом пути (в отличие от двух или трех сравнений и, следовательно, ветвей, как это делали предыдущие решения). Однако, как ранее утверждал

Можно ли предположить, что нам понадобится множество unsafe fn as_u32_unchecked(self) -> u32 и друзья, независимо от того, что показывает бенчмаркинг? Какой еще потенциальный выход был бы у кого-то, если бы он в конечном итоге наблюдал замедление?

@bstrie Я думаю, что в таком случае было бы as <type> [unchecked] и требовать, чтобы unchecked присутствовал только в unsafe контексты.

На мой взгляд, лес из функций _unchecked как вариантов приведения as был бы бородавкой, как с точки зрения интуитивности, так и когда дело доходит до создания чистой, удобной документации.

@ssokolow Добавление синтаксиса всегда должно быть крайней foo.as_unchecked::<u32>() было бы предпочтительнее синтаксических изменений (и сопутствующего бесконечного bikeshed), тем более что мы должны сокращать, а не увеличивать количество вещей, которые открывает unsafe .

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

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

Универсальный метод может поддерживаться новым набором признаков UncheckedFrom / UncheckedInto с методами unsafe fn , объединяющими From / Into и TryFrom / TryInto коллекция.

@bstrie Одним из альтернативных решений для людей, чей код стал медленнее, может быть использование встроенной функции (например, через stdsimd) для доступа к базовой инструкции оборудования. Ранее я утверждал, что это имеет недостатки для оптимизатора - вероятно, страдает автоматическая векторизация, и LLVM не может использовать ее, возвращая undef при вводе вне допустимого диапазона, но он предлагает способ выполнить приведение без любая дополнительная работа во время выполнения. Я не могу решить, достаточно ли это хорошо, но кажется, по крайней мере, правдоподобным, что это могло бы быть.

Некоторые примечания по преобразованиям в наборе инструкций x86:

SSE2 на самом деле относительно ограничен в том, какие операции преобразования он вам дает. У вас есть:

  • Семейство CVTTSS2SI с 32-битным регистром: преобразует одиночное число с плавающей запятой в i32
  • Семейство CVTTSS2SI с 64-битным регистром: преобразует одиночное число с плавающей запятой в i64 (только x86-64)
  • Семейство CVTTPS2PI: преобразует два числа с плавающей запятой в два i32s

У каждого из них есть варианты для f32 и f64 (а также варианты, которые округляются вместо усечения, но здесь это бесполезно).

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

В результате существующее («небезопасное») поведение:

  • Чтобы преобразовать в u32, компиляторы преобразуют в i64 и усекают полученное целое число. (Это приводит к странному поведению для значений вне диапазона, но это UB, так что кого это волнует.)
  • Чтобы преобразовать во что-либо 16-битное или 8-битное, компиляторы преобразуют в i64 или i32 и усекают полученное целое число.
  • Чтобы преобразовать в u64, компиляторы генерируют болото инструкций. Для f32 - u64 GCC и LLVM генерируют эквивалент:
fn f32_to_u64(f: f32) -> u64 {
    const CUTOFF: f32 = 0x8000000000000000 as f32; // 2^63 exactly
    if !(f >= CUTOFF) { // less, or NaN
        // just use the signed conversion
        f as i64 as u64
    } else {
        0x8000000000000000u64 + ((f - CUTOFF) as i64 as u64)
    }
}

Несвязанный забавный факт: генерация кода «преобразовать вместо усечения» - это то, что вызывает сбой « параллельных вселенных » в Super Mario 64. Первая инструкция MIPS кода обнаружения столкновений для преобразования координат f32 в i32, а затем усечения до i16; таким образом, координаты, которые вписываются в i16, но не i32 'wrap', например, переход к координате 65536.0 дает вам обнаружение столкновения для 0,0.

Так или иначе, выводы:

  • «Проверить на 0x80000000 и иметь специальный обработчик» работает только для преобразований в i32 и i64.
  • Однако для преобразований в u32, u / i16 и u / i8 эквивалент «проверить, отличается ли усеченный / расширенный по знаку вывод от оригинала». (Это приведет к сбору как целых чисел, которые были в диапазоне для исходного преобразования, но вне диапазона для окончательного типа, так и 0x8000000000000000, индикатора того, что число с плавающей запятой было NaN или вне диапазона для исходного преобразования.)
  • Но стоимость ветки и кучи дополнительного кода для этого случая, вероятно, слишком велика. Это может быть нормально, если можно избежать ветвей.
  • Подход @ActuallyaDeviloper, основанный на minss / maxss, не так уж плох! Минимальная форма,
minss %xmm2, %xmm1
maxss %xmm3, %xmm1
cvttss2si %rax, %xmm1

всего три инструкции (с приличным размером кода и пропускной способностью / задержкой) и без ветвей.

Тем не мение:

  • Версия на чистом Rust требует дополнительного тестирования на NaN. Для преобразований в 32-разрядную или меньшую версию этого можно избежать, используя встроенные функции, используя 64-разрядную версию cvttss2si и усекая результат. Если вход не был NaN, минимальное / максимальное значение гарантирует, что целое число не изменится путем усечения. Если вход был NaN, целое число равно 0x8000000000000000, которое усекается до 0.
  • Я не включил стоимость загрузки 2147483647.0 и -2148473648.0 в регистры, обычно по одному перемещению из памяти каждый.
  • Для f32 2147483647.0 не может быть представлен точно, поэтому на самом деле это не работает: вам нужна еще одна проверка. Это только усугубляет ситуацию. То же самое для f64 и u / i64, но от f64 до u / i32 этой проблемы нет.

Предлагаю компромисс между двумя подходами:

  • Для f32 / f64 до u / i16 и u / i8 и от f64 до u / i32 используйте усечение min / max +, как указано выше, например:
    let f = if f > 32767.0 { 32767.0 } else { f };
    let f = if f < -32768.0 { -32768.0 } else { f };
    cvttss2si(f) as i16

(Для u / i16 и u / i8 исходное преобразование может быть в i32; для f64 в u / i32 оно должно быть в i64.)

  • Для f32 / 64 - u32,
    let r = cvttss2si64(f) as u32;
    if f >= 4294967296.0 { 4294967295 } else { r }

это всего несколько инструкций и никаких веток:

    cvttss2si   %xmm0, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movl    $-1, %eax
    cmovbl  %ecx, %eax
  • Для f32 / 64 на i64 возможно
    let r = cvttss2si64(f);
    if f >= 9223372036854775808. {
        9223372036854775807 
    } else if f != f {
        0
    } else {
        r
    }

Это дает более длинную (все еще безветвую) последовательность:

    cvttss2si   %xmm0, %rax
    xorl    %ecx, %ecx
    ucomiss %xmm0, %xmm0
    cmovnpq %rax, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movabsq $9223372036854775807, %rax
    cmovbq  %rcx, %rax

… Но, по крайней мере, мы сохраняем одно сравнение по сравнению с наивным подходом, как если бы f слишком мало, 0x8000000000000000 уже является правильным ответом (т.е. i64 :: MIN).

  • Для f32 на i32, не уверен, что было бы предпочтительнее сделать то же самое, что и предыдущее, или просто сначала преобразовать в f64, а затем сделать более короткую вещь min / max.

  • u64 - беспорядок, о котором я не хочу думать. :п

В https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 кто-то сообщил об измеримом и значительном замедлении кодирования JPEG с помощью ящика изображений. Я минимизировал программу, чтобы она была автономной и в основном сосредоточена на частях, связанных с замедлением: https://gist.github.com/rkruppe/4e7972a209f74654ebd872eb4bc57722 (эта программа показывает замедление на ~ 15% для меня с насыщением слепки).

Обратите внимание, что приведение типов f32-> u8 ( rgb_to_ycbcr ) и f32-> i32 ( encode_rgb , цикл «квантование») в равных пропорциях. Также похоже, что все входные данные находятся в пределах допустимого диапазона, т.е. насыщение никогда не вступает в силу, но в случае f32-> u8 это можно проверить только путем вычисления минимума и максимума полинома и учета ошибки округления, которая есть много вопросов. Приведения f32-> i32 более очевидно находятся в диапазоне для i32, но только потому, что элементы self.tables отличны от нуля, что (по-видимому?) Не так просто для оптимизатора показать, особенно в исходной программе. tl; dr: Проверки насыщения должны остаться, единственная надежда - сделать их быстрее.

Я также немного потрогал LLVM IR - похоже, буквально единственная разница - это сравнения и выбор из насыщающих приведений. Беглый взгляд показывает, что asm имеет соответствующие инструкции и, конечно же, множество живых значений (которые приводят к большему количеству разливов).

@comex Считаете ли вы, что преобразование f32-> u8 и f32-> i32 можно сделать заметно быстрее с помощью CVTTSS2SI?

Незначительное обновление, начиная с rustc 1.28.0-nightly (952f344cd 2018-05-18) , флаг -Zsaturating-float-casts прежнему приводит к тому, что код в https://github.com/rust-lang/rust/issues/10184#issuecomment -345479698 составляет ~ 20 % медленнее на x86_64. Это означает, что LLVM 6 ничего не изменил.

| Флаги | Сроки |
| ------- | -------: |
| -Copt-level = 3 -Ctarget-cpu = native | 325 699 нс / л (+/- 7 607) |
| -Copt-level = 3 -Ctarget-cpu = native -Zsaturating-float-castts | 386 962 нс / л (+/- 11 601)
(На 19% медленнее) |
| -Copt-level = 3 | 331 521 нс / л (+/- 14 096) |
| -Copt-level = 3 -Zsaturating-float-cast | 413,572 нс / л (+/- 19,183)
(На 25% медленнее) |

@kennytm Мы ожидали, что LLVM 6 что-то изменит? Обсуждают ли они конкретное усовершенствование, которое принесет пользу этому варианту использования? Если да, то какой у билета номер?

@insanitybit Он ... кажется все еще открыт ...?

image

Хорошо, понятия не имею, на что я смотрел. Благодаря!

@rkruppe разве мы не позаботились о том, чтобы
(изменяя документы)?

20 июля 2018 г. в 4:31 «Колин» [email protected] написал:

Хорошо, понятия не имею, на что я смотрел.

-
Вы получаете это, потому что подписаны на эту ветку.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/10184#issuecomment-406462053 ,
или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AApc0v3rJHhZMD7Kv7RC8xkGOiIhkGB1ks5uITMHgaJpZM4BJ45C
.

@nagisa Может быть, вы думаете о f32::from_bits(v: u32) -> f32 (и аналогично f64 )? Раньше он выполнял некоторую нормализацию NaN, но теперь это просто transmute .

Эта проблема касается конверсий as которые пытаются приблизить числовое значение.

@nagisa Возможно, вы думаете о приведении типов float-> float, см. # 15536 ​​и https://github.com/rust-lang-nursery/nomicon/pull/65.

Ах да, это было плавать, чтобы плавать.

Пт, 20 июля 2018 г., 12:24 Робин Круппе [email protected] написал:

@nagisa https://github.com/nagisa Возможно, вы думаете о float-> float
приведений, см. # 15536 https://github.com/rust-lang/rust/issues/15536 и
ржавчина-питомник / номикон # 65
https://github.com/rust-lang-nursery/nomicon/pull/65 .

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/10184#issuecomment-406542903 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AApc0gA24Hz8ndnYhRXCyacd3HdUSZjYks5uIaHegaJpZM4BJ45C
.

В примечаниях к выпуску LLVM 7 упоминается следующее:

Улучшена оптимизация приведения типов с плавающей запятой. Это может привести к неожиданным результатам для кода, который полагается на неопределенное поведение переполняющих приведений. Оптимизацию можно отключить, указав атрибут функции: "strict-float-cast-overflow" = "false". Этот атрибут может быть создан с помощью опции clang -fno-strict-float-cast-overflow. Дезинфицирующие средства кода можно использовать для обнаружения затронутых шаблонов. Параметр clang для обнаружения этой проблемы - -fsanitize = float-cast-overflow:

Имеет ли это какое-либо отношение к этому вопросу?

Нам не нужно заботиться о том, что LLVM делает с переполнением приведений, если это не небезопасное неопределенное поведение. Результатом может быть мусор, если он не может вызывать ненадлежащее поведение.

Имеет ли это какое-либо отношение к этому вопросу?

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

@rkruppe из любопытства, https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 прошел достаточно хорошо, и в реализации не было слишком много ошибок. Кажется, всегда ожидалось небольшое снижение производительности, но правильная компиляция кажется стоящим компромиссом.

Это просто ожидание, чтобы пересечь финишную черту? Или есть другие известные блокаторы?

В основном я был отвлечен / занят другими делами, но регресс x0,82 в кодировке RBG JPEG кажется более чем «незначительным», довольно горькой пилюлей, которую нужно проглотить (хотя это обнадеживает, что другие виды рабочей нагрузки, похоже, не затронуты) . Это недостаточно серьезно, чтобы я возражал против включения насыщенности по умолчанию, но достаточно, чтобы я не решался настаивать на этом сам, прежде чем мы попробуем "также предоставить функцию преобразования, которая быстрее, чем насыщение, но может генерировать (безопасный) мусор "вариант обсуждался ранее. Я еще не дошел до этого, и, по-видимому, никто другой тоже, так что это отошло на второй план.

Хорошо, круто, спасибо за обновление @rkruppe! Мне любопытно, есть ли на самом деле реализация безопасного мусора? Я мог бы представить, что мы легко предоставляем что-то вроде unsafe fn i32::unchecked_from_f32(...) и тому подобное, но похоже, что вы думаете, что это должна быть безопасная функция. Возможно ли это с помощью LLVM сегодня?

freeze пока нет, но можно использовать встроенную сборку для доступа к инструкции целевой архитектуры для преобразования чисел с плавающей запятой в целые числа (с откатом, например, к насыщению as ). Хотя это может препятствовать некоторым оптимизациям, этого может быть достаточно, чтобы в основном исправить регресс в некоторых тестах.

Функция unsafe которая сохраняет UB, о котором идет речь (и кодируется так же, как as сегодня), является другим вариантом, но гораздо менее привлекательным, я ' Я предпочитаю безопасную функцию, если она может выполнять свою работу.

Также есть значительные возможности для улучшения последовательности безопасного насыщения от поплавка к внутреннему объему . Сегодня в LLVM нет ничего специально для этого, но если есть встроенные asm-решения, сделать что-то вроде этого не составит труда:

     cvttsd2si %xmm0, %eax   # x86's cvttsd2si returns 0x80000000 on overflow and invalid cases
     cmp $1, %eax            # a compact way to test whether %eax is equal to 0x80000000
     jno ok
     ...  # slow path: check for and handle overflow and invalid cases
ok:

что должно быть значительно быстрее, чем то, что сейчас делает rustc .

Хорошо, я просто хотел прояснить, спасибо! Я полагал, что встроенные asm-решения не работают по умолчанию, так как они слишком сильно тормозят другие оптимизации, но я не пробовал себя. Я лично предпочел бы, чтобы мы закрыли эту ненадежную дыру, определив какое-то разумное поведение (как в точности сегодняшние насыщающие приведения). При необходимости мы всегда можем сохранить сегодняшнюю быструю / ненадежную реализацию как небезопасную функцию, а в ограниченное время, учитывая бесконечные ресурсы, мы можем даже радикально улучшить стандартное значение и / или добавить другие специализированные функции преобразования (например, безопасное преобразование, где выход за пределы не UB, а просто шаблон мусора)

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

Я думаю, что встроенная сборка должна быть терпимой для cvttsd2si (или аналогичных инструкций), особенно потому, что этот встроенный asm не будет обращаться к памяти или иметь побочные эффекты, поэтому это просто непрозрачный черный ящик, который можно удалить, если он не используется и не очень сильно препятствует оптимизации вокруг него, LLVM просто не может рассуждать о внутренностях и результирующем значении встроенного asm. Этот последний бит - вот почему я скептически отношусь, например, к использованию встроенного asm для кодовой последовательности, которую @sunfishcode предлагает для насыщения: проверки, введенные для насыщения, могут иногда быть удалены сегодня, если они избыточны, но ветви во встроенном блоке asm могут t быть упрощенным.

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

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

Я начал некоторую работу по реализации встроенных функций для насыщения типов float до int в LLVM: https://reviews.llvm.org/D54749

Если это произойдет куда-нибудь, это обеспечит относительно малозатратный способ получения насыщающей семантики.

Как воспроизвести это неопределенное поведение? Я попробовал пример из комментария, но результат был 255 , что мне кажется нормальным:

println!("{}", 1.04E+17 as u8);

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

Короче говоря, используемому нами механизму генерации кода (LLVM) разрешено предполагать, что этого не происходит, и поэтому он может генерировать плохой код, если когда-либо будет полагаться на это предположение.

@ AaronM04 пример воспроизводимого неопределенного поведения был опубликован сегодня

fn main() {
    let a = 360.0f32;
    println!("{}", a as u8);

    let a = 360.0f32 as u8;
    println!("{}", a);

    println!("{}", 360.0f32 as u8);
}

(см. детскую площадку )

Я предполагаю, что последний комментарий был предназначен для @ AaronM04 со ссылкой на их предыдущий комментарий .

«О, тогда это достаточно просто».

  • @pcwalton , 2014

Извините, я очень внимательно прочитал всю эту 6-летнюю историю добрых намерений. А если серьезно, 6 долгих лет из 10 !!! Если бы это был форум политиков, можно было бы ожидать здесь какой-нибудь пылающий саботаж.

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

Потому что это сложнее, чем казалось изначально, и требует изменений LLVM.

Хорошо, но не Бог создал этот LLVM на своей второй неделе, и, двигаясь в том же направлении, может потребоваться еще 15 лет, чтобы решить эту фундаментальную проблему.

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

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

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

Я думаю, что мы можем сделать это на языке с минимальными затратами на производительность. Было бы это решающим изменением * * да, но это можно сделать, и это нужно сделать.

Моим решением было бы определить приведение float к int как unsafe затем предоставить некоторые вспомогательные функции в стандартной библиотеке для предоставления результатов, привязанных к типам Result .

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

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

Как отмечалось ранее в потоке, это медленно, но верно исправляется Правильным путем путем исправления llvm для поддержки необходимой семантики, о чем давно договорились соответствующие команды.

Больше ничего к этому обсуждению добавить нельзя.

https://reviews.llvm.org/D54749

@nikic Похоже, прогресс на стороне LLVM застопорился, не могли бы вы дать краткую информацию, если возможно? Благодарю.

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

https://github.com/rust-lang/rust/blob/625451e376bb2e5283fc4741caa0a3e8a2ca4d54/src/librustc_codegen_ssa/mir/rvalue.rs#L774 -L901

Мы могли бы предоставить внутреннюю функцию, которая генерирует LLVM IR для насыщения (будь то текущая IR с открытым кодом или llvm.fpto[su]i.sat в будущем) независимо от флага -Z . Это совсем не сложно.

Однако меня беспокоит, будет ли это лучшим вариантом. Когда (если?) Насыщение становится семантикой по умолчанию для приведений as , такой API становится избыточным. Также кажется неуместным говорить пользователям, что они должны сами выбирать, хотят ли они надежности или производительности, даже если это временно.

В то же время сейчас ситуация явно еще хуже. Если мы думаем о добавлении библиотечных API, я все больше и больше предупреждаю, чтобы просто включить насыщение по умолчанию и предложить встроенные функции unsafe которые имеют UB на NaN и числах вне диапазона (и понижаются до простой fpto[su]i ). Это по-прежнему будет предлагать в основном тот же выбор, но по умолчанию к разумности, и новый API, вероятно, не станет избыточным в будущем.

Переход на звук по умолчанию звучит неплохо. Я думаю, что мы можем предложить внутреннюю функцию лениво по запросу, а не с самого начала. Кроме того, будет ли const eval также выполнять насыщенность в этом случае? (cc @RalfJung @eddyb @ oli-obk)

Const eval us уже выполняет насыщение, и делал это целую вечность, я думаю, еще до miri (я отчетливо помню, как менял его в старом оценщике на основе llvm::Constant ).

@rkruppe Замечательно ! Поскольку вы знакомы с рассматриваемым кодом, не хотели бы вы возглавить переключение значений по умолчанию?

@rkruppe

Мы могли бы выставить внутреннюю функцию, которая генерирует LLVM IR для насыщения

Может потребоваться 10 или 12 отдельных встроенных функций для каждой комбинации типа источника и назначения.

@Centril

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

Я предполагаю, что в отличие от других комментариев, «внутреннее» в вашем комментарии означает что-то, что будет иметь меньшую регрессию pref, когда as выполняет насыщение.

Я не думаю, что это хороший подход к известным значительным регрессам. Для некоторых пользователей потеря производительности может быть реальной проблемой, в то время как их алгоритм гарантирует, что вход всегда находится в диапазоне. Если они не подписаны на этот поток, они могут понять, что затронуты, только когда изменение достигнет стабильного канала. В этот момент они могут застрять на 6–12 недель, даже если мы сразу же по запросу получим небезопасный API.

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

Может потребоваться 10 или 12 отдельных встроенных функций для каждой комбинации типа источника и назначения.

Хорошо, вы меня поняли, но я не понимаю, насколько это актуально? Пусть будет 30 встроенных функций, добавить их еще нетривиально. Но на самом деле еще проще иметь одну общую внутреннюю функцию, используемую N тонкими оболочками. Число также не изменится, если мы выберем вариант «сделать as sound и ввести unsafe cast API».

Я не думаю, что это хороший способ справиться с _ известными_ значительными регрессами. Для некоторых пользователей потеря производительности может быть реальной проблемой, в то время как их алгоритм гарантирует, что вход всегда находится в диапазоне. Если они не подписаны на этот поток, они могут понять, что затронуты, только когда изменение достигнет стабильного канала. В этот момент они могут застрять на 6–12 недель, даже если мы сразу же по запросу получим небезопасный API.

+1

Я не уверен, нужна ли процедура для предупреждений об устаревании (не рекомендуется использовать только в ночное время, когда замена станет стабильной), поскольку кажется менее важным оставаться без регрессии производительности во всех каналах выпуска, чем оставаться без предупреждений во всех каналах выпуска , но опять же, ожидание еще 12 недель - это, по сути, ошибка округления с учетом того, как долго существует эта проблема.

Мы также можем оставить -Zsaturating-float-casts (просто изменив значение по умолчанию), что означает, что любой ночной пользователь может на некоторое время отказаться от использования канала.

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

@rkruppe я не могу утверждать, что усваиваются все комментарии здесь, но я нахожусь под впечатлением , что LLVM в настоящее время действительно есть инструкции замораживания, который был пункт блокирует «кратчайший путь» для устранения UB здесь, не так ли?

Хотя я полагаю, что freeze настолько нов, что может быть недоступен в нашей собственной версии LLVM, верно? Тем не менее, кажется, что мы должны изучить возможность разработки, возможно, в первой половине 2020 года?

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

Использование freeze по-прежнему проблематично по всем причинам, указанным здесь . Я не уверен, насколько реалистичны такие опасения по поводу использования замораживания для этих приведений, но в принципе они применимы. По сути, ожидайте, что freeze вернет либо случайный мусор, либо ваш секретный ключ, что хуже. (Я где-то читал это в Интернете, и мне очень нравится его краткое содержание .: D)

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

@SimonSapin вы

@pnkfelix

У меня сложилось впечатление, что в LLVM теперь есть инструкция по замораживанию, которая была элементом, блокирующим «кратчайший путь» к удалению UB, верно?

Есть некоторые предостережения. Что наиболее важно, даже если все, о чем мы заботимся, это избавиться от UB и обновить наш связанный LLVM, включив в него freeze (что мы можем сделать в любое время), мы поддерживаем несколько более старых версий (вернемся к LLVM 6 на момент), и нам понадобится некоторая резервная реализация, чтобы действительно избавиться от UB для всех пользователей.

Во-вторых, конечно, это вопрос, является ли «просто не UB» всем, что нас волнует, пока мы этим занимаемся. В частности, я хочу еще раз подчеркнуть, что freeze(fptosi %x) ведет себя крайне противоречиво: он недетерминирован и может возвращать другой результат (даже тот, который взят из чувствительной памяти, как сказал @RalfJung ) каждый раз при выполнении. Я не хочу сейчас снова обсуждать этот вопрос, но на собрании стоит подумать о том, не предпочтем ли мы немного больше поработать, чтобы сделать насыщенность преобразованиями по умолчанию и без флажков (небезопасных или freeze -используемых). вариант не по умолчанию.

@RalfJung Моя позиция заключается в том, что as лучше всего избегать полностью независимо от этой проблемы, потому что он может иметь совершенно разную семантику (усечение, насыщение, округление и т. Д.) В зависимости от типа ввода и вывода, а это не всегда очевидно при чтении кода. (Даже последнее можно вывести с помощью foo as _ .) Итак, у меня есть предварительный проект RFC для предложения различных явно названных методов преобразования, которые охватывают случаи, которые as делает сегодня (и, возможно, больше) .

Я думаю, что в as определенно не должно быть UB, поскольку его можно использовать вне unsafe . Возвращать мусор тоже не очень хорошо. Но у нас, вероятно, должно быть какое-то смягчение / переход / альтернатива для известных случаев снижения производительности, вызванных насыщающим приведением. Я спросил только о библиотечной реализации насыщающего приведения, чтобы не блокировать этот черновик RFC при этом переходе.

@SimonSapin

Моя позиция заключается в том, что этого лучше всего избегать полностью независимо от этой проблемы, потому что она может иметь совершенно разную семантику (усечение, насыщение, округление и т. Д.)

Согласовано. Но это не совсем помогает нам в этом вопросе.

(Кроме того, я рад, что вы работаете над тем, чтобы as не понадобились. С нетерпением жду этого.: D)

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

Итак, мы, кажется, согласны с тем, что конечным состоянием должно быть то, что float-to-int as насыщает? Я доволен любым планом перехода, если это конечная цель, к которой мы движемся.

Мне нравится эта конечная цель.

Я не думаю, что это хороший способ справиться с _ известными_ значительными регрессами. Для некоторых пользователей потеря производительности может быть реальной проблемой, в то время как их алгоритм гарантирует, что вход всегда находится в диапазоне. Если они не подписаны на этот поток, они могут понять, что затронуты, только когда изменение достигнет стабильного канала. В этот момент они могут застрять на 6–12 недель, даже если мы сразу же по запросу получим небезопасный API.

На мой взгляд, это не будет концом света, если эти пользователи будут ждать с обновлением своего rustc в течение этих 6-12 недель - им может не понадобиться ничего из предстоящих выпусков в любом случае, или их библиотеки могут иметь ограничения MSRV для поддерживать.

Между тем, пользователи, которые также не подписаны на поток, могут сталкиваться с ошибками компиляции, так же как и с потерей производительности. Что мы должны сделать в первую очередь? Мы даем гарантии стабильности и безопасности - но, насколько мне известно, таких гарантий в отношении производительности не дается (например, RFC 1122 вообще не упоминает perf).

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

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

Хорошо, вы меня поняли, но я не понимаю, насколько это актуально? Пусть будет 30 встроенных функций, добавить их еще нетривиально. Но на самом деле еще проще иметь одну общую внутреннюю функцию, используемую N тонкими оболочками. Число также не изменится, если мы выберем вариант «сделать as sound и ввести unsafe cast API».

Разве для этой единственной универсальной встроенной функции не потребуются отдельные реализации в компиляторе для тех конкретных мономорфных экземпляров 12/30?

Добавление встроенных функций к компилятору может оказаться тривиальным делом, поскольку LLVM уже проделал большую часть работы, но это также далеко не полная стоимость. Кроме того, есть реализация в Miri, Cranelift, а также возможная работа, необходимая для спецификации. Поэтому я не думаю, что мы должны добавлять встроенные функции на случай, если они кому-то понадобятся.

Я, однако, не против раскрытия большего количества внутренних функций, но если они кому-то нужны, они должны сделать предложение (например, в качестве PR с некоторым подробным описанием) и обосновать добавление некоторыми контрольными числами или чем-то подобным.

Мы также можем оставить -Zsaturating-float-casts (просто изменив значение по умолчанию), что означает, что любой ночной пользователь может на некоторое время отказаться от использования канала.

Мне это кажется нормальным, но я бы предложил переименовать флаг в -Zunsaturating-float-casts чтобы избежать изменения семантики в сторону несостоятельности для тех, кто уже использует этот флаг.

@Centril

Разве для этой единственной универсальной встроенной функции не потребуются отдельные реализации в компиляторе для тех конкретных мономорфных экземпляров 12/30?

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

(Edit: чтобы быть ясным, это совместное использование может произойти, даже если есть N различных встроенных функций, но одна общая внутренняя функция сокращает шаблон, необходимый для каждой встроенной функции.)

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

Я, однако, не против раскрытия большего количества внутренних функций, но если они кому-то нужны, они должны сделать предложение (например, в качестве PR с некоторым подробным описанием) и обосновать добавление некоторыми контрольными числами или чем-то подобным. Я не думаю, что это должно помешать исправлению дыры в надежности.

У нас уже есть некоторые контрольные цифры. Мы уже давно знаем из призывов к тестам, что кодирование JPEG становится значительно медленнее на x86_64 с насыщающими приведениями. Кто-то может повторно запустить их, но я уверен, что предсказываю, что это не изменилось (хотя, конечно, конкретные числа не будут идентичными), и не вижу причин, по которым будущие изменения в том, как реализована насыщенность (например, переключение на встроенный asm или Встроенные функции LLVM, над @nikic ) в корне изменили бы это. Хотя трудно быть уверенным в будущем, мое обоснованное предположение состоит в том, что единственный правдоподобный способ вернуть эту производительность - это использовать что-то, что генерирует код без проверки диапазона, например преобразование unsafe или что-то еще с использованием freeze .

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

  1. Одновременно:

    • Представьте встроенные функции, отображаемые каждую ночь с помощью функций #[unstable(...)] .

    • Удалите -Zsaturating-float-casts и введите -Zunsaturating-float-casts .

    • Измените значение по умолчанию на то, что делает -Zsaturating-float-casts .

  2. Через некоторое время мы стабилизируем внутренние свойства; мы можем немного ускорить процесс.
  3. Через некоторое время удалите -Zunsaturating-float-casts .

Звучит неплохо. За исключением того, что встроенные функции являются деталями реализации некоторого общедоступного API, возможно, методами f32 и f64 . Они могут быть:

  • Методы универсального признака (с параметром для целочисленного возвращаемого типа преобразования), необязательно в прелюдии
  • Собственные методы с поддерживающим свойством (аналогично str::parse и FromStr ) для поддержки различных типов возврата
  • Несколько неуниверсальных внутренних методов с целевым типом в имени

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

Несколько неуниверсальных внутренних методов с целевым типом в имени

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

Это правда? Я чувствую, что когда имя типа (подписи) является частью имени метода, это специальные «единственные в своем роде» преобразования (например, Vec::as_slice и [T]::to_vec ) или серию преобразований, в которых разница не является типом (например, to_ne_bytes , to_be_bytes , to_le_bytes ). Но отчасти мотивация для черт std::convert заключалась в том, чтобы избегать множества отдельных методов, таких как u8::to_u16 , u8::to_u32 , u8::to_u64 и т. Д.

Меня интересует, можно ли это естественным образом обобщить на черту, учитывая, что методы должны быть unsafe fn . Если мы добавим собственные методы, то вы всегда сможете делегировать их тем, которые используются в реализациях трейтов, и тому подобное.

Мне действительно кажется странным добавлять признаки для небезопасных преобразований, но я думаю, Саймон думает о том, что нам, возможно, понадобится другой метод для каждой комбинации типа с плавающей запятой и целочисленного типа (например, f32::to_u8_unsaturated , f32::to_u16_unsaturated и т. Д.).

Чтобы не вдаваться в длинную цепочку, я не читал в полном незнании, но желательно ли это или достаточно иметь, например, f32::to_integer_unsaturated который конвертируется в u32 или что-то в этом роде? Есть ли очевидный выбор целевого типа для небезопасного преобразования?

Предоставление небезопасных преобразований только в i32 / u32 (например) полностью исключает все целочисленные типы, диапазон значений которых не строго меньше, а это определенно иногда необходимо. Уменьшение размера (до u8, как в кодировке JPEG) также часто необходимо, но его можно эмулировать путем преобразования в более широкий целочисленный тип и усечения с помощью as (что дешево, но обычно не бесплатно).

Но мы не можем обеспечить преобразование только в самый большой целочисленный размер. Они не всегда изначально поддерживаются (следовательно, медленные), и оптимизация не может это исправить: нецелесообразно оптимизировать «преобразовать в большой int, затем усечь» в «преобразовать в меньший int напрямую», потому что последний имеет UB (в LLVM IR) / разные результаты (на уровне машинного кода, на большинстве архитектур) в тех случаях, когда исходный результат преобразования был бы обернут вокруг при усечении.

Обратите внимание, что даже прагматическое исключение 128-битных целых чисел и сосредоточение внимания на 64-битных целых числах все равно будет плохо для обычных 32-битных целей.

Я новичок в этом разговоре, но не в программировании. Мне любопытно, почему люди думают, что насыщение конверсий и преобразование NaN в ноль являются разумным поведением по умолчанию. Я понимаю, что это делает Java (хотя обертывание кажется гораздо более распространенным), но нет целочисленного значения, для которого NaN действительно можно назвать правильным преобразованием. Точно так же преобразование 1000000.0 в 65535 (u16), например, кажется неправильным. U16 просто не существует, это явно правильный ответ. По крайней мере, я не думаю, что это лучше, чем текущее поведение при преобразовании его в 16960, которое, по крайней мере, является поведением, общим для C / C ++, C #, go и другими, и поэтому, по крайней мере, несколько неудивительно.

Различные люди отмечали сходство с проверкой переполнения, и я согласен с ними. Это также похоже на целочисленное деление на ноль. Я думаю, что недопустимые преобразования должны вызывать панику, как и неправильная арифметика. Полагаться на NaN -> 0 и 1000000.0 -> 65535 (или 16960) кажется столь же подверженным ошибкам, как и полагаться на целочисленное переполнение или гипотетическое n / 0 == 0. Это тот тип вещей, который по умолчанию должен вызывать ошибку. (В сборках выпуска ржавчина может исключить проверку ошибок, как и в случае с целочисленной арифметикой.) И в тех редких случаях, когда вы _ хотите_ преобразовать NaN в ноль или иметь насыщенность с плавающей запятой, вам следует выбрать ее, как и вы должны выбрать целочисленное переполнение.

Что касается производительности, похоже, что самая высокая общая производительность будет достигнута при простом преобразовании и использовании аппаратных сбоев. И x86, и ARM, например, вызывают аппаратные исключения, когда преобразование с плавающей запятой в целое число не может быть правильно представлено (включая случаи, когда NaN и выход за пределы допустимого диапазона). Это решение имеет нулевую стоимость, за исключением недопустимых преобразований, за исключением случаев прямого преобразования типов с плавающей запятой в малые целые числа в отладочных сборках - редкий случай - где оно должно быть сравнительно дешевым. (На теоретическом оборудовании, которое не поддерживает эти исключения, его можно эмулировать в программном обеспечении, но опять же только в отладочных сборках.) Я полагаю, что аппаратные исключения - это именно то, как сегодня реализовано обнаружение целочисленного деления на ноль. Я видел много разговоров о LLVM, так что, возможно, вы здесь ограничены, но было бы неудачно иметь программную эмуляцию при каждом преобразовании с плавающей запятой даже в сборках релиза, чтобы обеспечить сомнительное альтернативное поведение для изначально недействительных преобразований.

@admilazz Мы ограничены тем, что может делать LLVM, и в настоящее время LLVM не предоставляет метод для эффективного преобразования чисел с плавающей запятой в целые числа без риска неопределенного поведения.

Насыщенность обусловлена ​​тем, что язык определяет приведение типов as всегда успешно, и поэтому мы не можем вместо этого изменить оператор на panic.

Точно так же преобразование 1000000.0 в 65535 (u16), например, кажется неправильным. U16 просто не существует, это явно правильный ответ. По крайней мере, я не считаю это лучше, чем текущее поведение при преобразовании в 16960,

Для меня это не было очевидным, поэтому я думаю, что стоит указать: 16960 - это результат преобразования 1000000.0 в достаточно широкое целое число с последующим усечением, чтобы сохранить 16 младших битов.

Это ~ не вариант, который ранее предлагался в этой ветке, и ~ (Edit: я ошибся здесь, извините, я не нашел его) не текущее поведение. Текущее поведение в Rust заключается в том, что преобразование числа с плавающей точкой в ​​целое число вне допустимого диапазона является неопределенным поведением. На практике это часто приводит к мусорному значению, в принципе это может привести к ошибкам компиляции и уязвимостям. Эта ветка посвящена тому, как это исправить. Когда я запускаю приведенную ниже программу в Rust 1.39.0, я каждый раз получаю другое значение:

fn main() {
    dbg!(1000000.0 as u16);
}

Детская площадка . Пример вывода:

[src/main.rs:2] 1000000.0 as u16 = 49072

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

Я думаю, что нам также следует добавить ошибочные API преобразования, которые возвращают Result , но мне все еще нужно закончить написание этого предварительного RFC :)

«Обращенный к математическому целому числу, то усечение до целевой ширины» или «обволакивания» семантики было предложено ранее в этом потоке (https://github.com/rust-lang/rust/issues/10184#issuecomment-299229143). Мне это не особо нравится:

  • Думаю, это чуть менее разумно, чем насыщенность. Насыщенность обычно не дает заметные результаты для чисел далеко выходит за пределы диапазона, но:

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

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

  • AFAIK единственная причина предпочесть семантику переноса - эффективность программной эмуляции, но мне это кажется недоказанным предположением. Я был бы счастлив, если бы ошибся, но при беглом взгляде кажется, что требуется такая длинная цепочка инструкций ALU (плюс ветки для обработки бесконечностей и NaN по отдельности), что я не чувствую, что ясно, что один будет явно лучше для производительность, чем другие.
  • Хотя вопрос о том, что делать для NaN - уродливая проблема для любого преобразования в целое число, насыщение по крайней мере не требует какого-либо специального корпуса (ни в семантике, ни в большинстве реализаций) для бесконечности. Но что касается переноса, какой целочисленный эквивалент должен быть +/- бесконечность? JavaScript говорит, что это 0, и я полагаю, что если бы мы сделали as panic на NaN, тогда он также мог бы паниковать до бесконечности, но в любом случае это, похоже, усложнит быстрый переход, чем просмотр нормальных и денормальных чисел один только предложил бы.

Я подозреваю, что большую часть кода, регрессировавшего семантикой насыщения для преобразования, было бы лучше использовать SIMD. Таким образом, хотя это изменение, к сожалению, не помешает написанию высокопроизводительного кода (особенно, если предоставляется встроенный с другой семантикой), и может даже подтолкнуть некоторые проекты к более быстрой (хотя и менее переносимой) реализации.

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

https://github.com/rust-lang/rust/pull/66841 добавляет методы unsafe fn которые конвертируются с помощью LLVM fptoui и fptosi , для тех случаев, когда значения известны быть в пределах допустимого диапазона, а насыщение - это измеримое снижение производительности.

После этого, я думаю, можно изменить значение по умолчанию для as (и, возможно, добавить еще один флаг -Z чтобы отказаться?), Хотя это, вероятно, должно быть формальным решением команды Lang.

После этого, я думаю, можно изменить значение по умолчанию для as (и, возможно, добавить еще один флаг -Z чтобы отказаться?), Хотя это, вероятно, должно быть формальным решением команды Lang.

Итак, мы (языковая группа, по крайней мере, с людьми, которые там были) обсудили это на https://github.com/rust-lang/lang-team/blob/master/minutes/2019-11-21.md и подумали добавление новых встроенных функций + добавление -Zunsaturated-float-casts было бы хорошими первыми шагами.

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

Я предполагаю, что под новыми встроенными функциями вы имеете в виду что-то вроде https://github.com/rust-lang/rust/pull/66841

Что значит добавить -Z unsaturated-float-casts без изменения значения по умолчанию? Принять это как бездействие вместо выдачи «ошибка: неизвестный параметр отладки»?

Я предполагаю, что под новыми встроенными функциями вы подразумеваете что-то вроде # 66841

Ага 👍 - спасибо, что возглавили это.

Что значит добавить -Z unsaturated-float-casts без изменения значения по умолчанию? Принять это как бездействие вместо выдачи «ошибка: неизвестный параметр отладки»?

Да в основном. В качестве альтернативы мы удаляем -Z saturated-float-casts в пользу -Z unsaturated-float-casts и напрямую переключаем значение по умолчанию, но это должно привести к тому же результату при меньшем количестве PR.

Я действительно не понимаю "ненасыщенного" предложения. Если цель состоит в том, чтобы просто предоставить ручку для отказа от нового значения по умолчанию, проще просто изменить значение по умолчанию для существующего флага и больше ничего не делать. Если цель состоит в том, чтобы выбрать новое имя, которое более четко объясняет компромисс (несостоятельность), тогда «ненасыщенный» ужасен - я бы вместо этого предложил имя, которое включает «небезопасный», «UB» или аналогичный страшное слово, например -Z fix-float-cast-ub .

unchecked - термин с некоторым прецедентом в именах API.

@admilazz Мы ограничены тем, что может делать LLVM, и в настоящее время LLVM не предоставляет метод для эффективного преобразования чисел с плавающей запятой в целые числа без риска неопределенного поведения.

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

AFAIK единственная причина предпочесть семантику переноса - это эффективность программной эмуляции.

Я не думаю, что мы должны предпочесть либо обтекание, либо насыщение, поскольку оба они неверны, но обертывание по крайней мере имеет то преимущество, что является методом, используемым во многих языках, подобных ржавчине: C / C ++, C #, go, возможно, D и, конечно, больше, а также о текущем поведении ржавчины (по крайней мере, иногда). Тем не менее, я думаю, что «паника при недопустимых преобразованиях (возможно, только в отладочных сборках)» идеальна, точно так же, как мы это делаем для целочисленного переполнения и недопустимой арифметики, такой как деление на ноль.

(Интересно, что я получил 16960 на детской площадке . Но из других опубликованных примеров я вижу, что иногда ржавчина делает это по-другому ...)

Насыщенность обусловлена ​​тем, что язык определяет приведение типов как всегда успешное, и поэтому мы не можем вместо этого изменить оператор на panic.

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

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

Если он проверяется только в отладочных сборках, как это происходит с целочисленным переполнением, тогда, я думаю, мы можем извлечь лучшее из обоих миров: преобразования гарантированно будут правильными (в отладочных сборках), ошибки пользователя с большей вероятностью будут обнаружены, вы можете выбрать к странному поведению, например, зацикливанию и / или насыщению, если хотите, и производительность будет настолько хорошей, насколько это возможно.

Кроме того, кажется странным управлять этим с помощью переключателя командной строки. Это большой молоток. Разумеется, желаемое поведение преобразования вне допустимого диапазона зависит от специфики алгоритма, поэтому это то, что следует контролировать для каждого преобразования. Я бы предложил f.to_u16_sat () и f.to_u16_wrap () или аналогичные в качестве опций, и не иметь какой-либо опции командной строки, которая изменяет семантику кода. Это затруднило бы смешивание и сопоставление разных частей кода, и вы не можете понять, что что-то делает, читая это ...

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

Интересно, что на детской площадке я получил 16960 штук.

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

wraparound по крайней мере имеет то преимущество, что является методом, используемым многими языками, похожими на rust: C / C ++, C #, go, возможно D и, конечно же,

Это правда? По крайней мере, не в C и C ++, они имеют то же неопределенное поведение, что и Rust. Это не случайно, мы используем LLVM, который в первую очередь построен для clang, реализующего C и C ++. Вы уверены, что насчет C # и вперед?

Стандарт C11 https://port70.net/~nsz/c/c11/n1570.html#6.3.1.4

Когда конечное значение реального типа с плавающей запятой преобразуется в целочисленный тип, отличный от _Bool, дробная часть отбрасывается (т. Е. Значение обрезается до нуля). Если значение составной части не может быть представлено целочисленным типом, поведение не определено.

Операция остатка, выполняемая при преобразовании значения целочисленного типа в беззнаковый тип, не должна выполняться, когда значение реального плавающего типа преобразуется в беззнаковый тип. Таким образом, диапазон переносимых реальных плавающих значений равен (-1, Utype_MAX + 1).

Стандарт C ++ 17 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf#section.7.10

Prvalue типа с плавающей запятой может быть преобразовано в prvalue целочисленного типа. Преобразование обрезается, то есть дробная часть отбрасывается. Поведение не определено, если усеченное значение не может быть представлено в типе назначения.

Справочник по C # https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions

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

Так что это не UB, а просто «неопределенное значение».

@admilazz Между этим и целочисленным переполнением огромная разница: целочисленное переполнение нежелательно, но четко определено . Приведение с плавающей запятой - неопределенное поведение .

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

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

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

@admilazz Существует огромная разница между этим и целочисленным переполнением: целочисленное переполнение нежелательно, но четко определено. Приведение с плавающей запятой - это неопределенное поведение.

Конечно, но эта тема посвящена определению поведения. Если бы он был определен как производящий «неуказанное значение целевого типа», как в спецификации C #, на которую Аманьё упомянул выше, тогда он больше не был бы неопределенным (каким-либо опасным способом). Вы не можете легко использовать четко определенный характер целочисленного переполнения в практических программах, потому что это все равно вызовет панику в отладочных сборках. Точно так же значение, созданное недопустимым приведением в сборках выпуска, не должно быть предсказуемым или особенно полезным, потому что программы все равно практически не могут его использовать, если оно паникует в сборках отладки. Это фактически дает компилятору максимальные возможности для оптимизации, тогда как выбор такого поведения, как насыщение, ограничивает компилятор и может быть значительно медленнее на оборудовании без встроенных инструкций преобразования насыщения. (И это не похоже на то, что насыщенность явно правильная.)

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

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

Это правда? По крайней мере, не в C и C ++, у них такое же Undefined Behavior, что и в Rust ... Вы уверены, что насчет C # и вперед?

Справедливо. Я не читал все их спецификации; Я просто тестировал разные компиляторы. Вы правы в том, что сказать «все компиляторы, которые я пробовал, делают это таким образом» - это не то же самое, что сказать «спецификации языка определяют это таким образом». Но я все равно не спорю в пользу переполнения, а лишь указываю на то, что он кажется наиболее распространенным. Я действительно выступаю за преобразование, которое 1) защищает от "неправильных" результатов, например, 1000000.0 становится 65535 или 16960, по той же причине, по которой мы защищаем от целочисленного переполнения - это, скорее всего, ошибка, поэтому пользователям следует выбрать ее. и 2) обеспечивает максимальную производительность в сборках выпуска.

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

Неопределенное поведение означает, что оптимизаторы (предоставленные разработчиками LLVM, ориентированными на C и C ++) могут свободно предполагать, что этого никогда не произойдет, и преобразовывают код на основе этого предположения, включая удаление фрагментов кода, которые доступны только через неопределенное приведение или, как показано в этом примере , предполагая, что назначение должно быть вызвано, хотя на самом деле это не так, потому что вызов кода, который вызывается без первого вызова, будет неопределенным поведением.

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

Я бы утверждать , что все неопределенное поведение , так на этой основе.

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

Что ж, очень жаль, что LLVM таким образом влияет на дизайн ржавчины, но я только что прочитал часть справочника инструкций LLVM, и в нем упоминается операция «замораживания», упомянутая выше («... другой - дождаться, пока LLVM добавит заморозку. concept… "), что предотвратит неопределенное поведение на уровне LLVM. Привязан ли ржавчина к старой версии LLVM? Если нет, мы могли бы это использовать. Однако в их документации неясно точное поведение.

Если аргумент undef или яд, 'freeze' возвращает произвольное, но фиксированное значение типа 'ty'. В противном случае эта инструкция не выполняется и возвращает входной аргумент. При любом использовании значения, возвращаемого одной и той же инструкцией «замораживания», всегда будет соблюдаться одно и то же значение, в то время как разные инструкции «замораживания» могут давать разные значения.

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

Что ж, очень жаль, что LLVM таким образом влияет на дизайн ржавчины.

Не только разработчики LLVM пишут оптимизаторы. Дело в том, что, даже если разработчики rustc написали оптимизаторы, заигрывание с неопределенностью по своей сути является огромным шагом из-за возникающих свойств объединения оптимизаторов. Человеческий мозг просто не эволюционировал, чтобы «интуитивно осознавать потенциальную величину ошибки округления», когда рассматриваемое округление представляет собой эмерджентное поведение, созданное путем объединения проходов оптимизации.

Я не собираюсь с тобой не соглашаться. :-) Я очень надеюсь, что эта инструкция LLVM "заморозить" предоставляет нулевой способ избежать такого неопределенного поведения.

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

ИМО, такая семантика была бы плохим языковым дизайном, которого мы бы предпочли избегать.

Моя позиция состоит в том, что as лучше всего избегать полностью независимо от этой проблемы, потому что он может иметь совершенно разную семантику (усечение, насыщение, округление и т. Д.) В зависимости от типа ввода и вывода, и это не всегда очевидно, когда чтение кода. (Даже последнее можно вывести с помощью foo as _ .) Итак, у меня есть предварительный проект RFC для предложения различных явно названных методов преобразования, которые охватывают случаи, которые as делает сегодня (и, возможно, больше) .

Я закончил черновик! https://internals.rust-lang.org/t/pre-rfc-add-explicitly- named-numeric-conversion-apis/11395

Любая обратная связь приветствуется, но, пожалуйста, делайте ее во внутренних потоках, а не здесь.

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

Извините, что повторяюсь, но я думаю, что тот же аргумент применим к целочисленному переполнению. Если вы умножите несколько чисел, и результат будет переполнен, вы получите совершенно неправильный результат, который почти наверняка сделает недействительным вычисление, которое вы пытались выполнить, но это вызывает панику в отладочных сборках, и поэтому ошибка, вероятно, будет обнаружена. Я бы сказал, что числовое преобразование, которое дает совершенно неправильные результаты, также должно вызывать панику, потому что очень высока вероятность, что оно представляет собой ошибку в коде пользователя. (Случай типичной неточности с плавающей запятой уже обработан. Если вычисление дает 65535,3, это уже допустимо для преобразования этого в u16. Чтобы получить преобразование за пределы, вам обычно нужна ошибка в вашем коде, и если у меня есть ошибка, я хочу получить уведомление и исправить ее.)

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

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

Однако мы не говорим об умножении, мы говорим о приведениях. И да, то же самое относится и к целочисленному переполнению: приведение типов int к int никогда не вызывает паники, даже когда они переполняются. Это потому, что as по своей природе никогда не паникует, даже в отладочных сборках. Отклонение от этого для приведения типов с плавающей запятой в лучшем случае удивительно, а в худшем - опасно, так как правильность и безопасность небезопасного кода могут зависеть от определенных операций, не вызывающих паники.

Если вы хотите возразить, что дизайн as ошибочен, потому что он обеспечивает безошибочное преобразование между типами, где правильное преобразование не всегда возможно, я думаю, что большинство из нас согласится. Но это полностью выходит за рамки этого потока, который касается исправления преобразований float-to-int внутри существующей структуры приведения типов as . Они должны быть безошибочными, они не должны паниковать, даже в отладочных сборках. Поэтому, пожалуйста, либо предложите разумную (не связанную с freeze ) семантику без паники для приведения типов float-to-int, либо попробуйте начать новую дискуссию о перепроектировании as чтобы разрешить панику когда приведение выполняется с потерями (и делайте это последовательно для преобразований типа int-to-int и float-to-int), но последнее в этой проблеме не по теме, поэтому, пожалуйста, откройте новую ветку (в стиле до RFC) для этого.

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

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

  1. Паника обратно несовместима с замораживанием, поэтому нам нужно отклонить по крайней мере все предложения, связанные с паникой. Переход от UB к панике менее очевидно несовместим, хотя, как обсуждалось выше, есть и другие причины не вызывать панику as .
  2. Как я уже писал ранее ,
    > мы поддерживаем несколько более старых версий (на данный момент вернемся к LLVM 6), и нам потребуется некоторая резервная реализация, чтобы они действительно избавились от UB для всех пользователей.

Я согласен с @RalfJung, что создание только некоторых as бросает панику крайне нежелательно, но в остальном я не думаю, что этот пункт, сделанный

(Случай типичной неточности с плавающей запятой уже обработан. Если вычисление дает 65535,3, это уже допустимо для преобразования этого в u16. Чтобы получить преобразование за пределы, вам обычно нужна ошибка в вашем коде, и если у меня есть ошибка, я хочу получить уведомление и исправить ее.)

Для f32-> u16 может оказаться правдой, что вам нужна чрезвычайно большая ошибка округления, чтобы выпасть из диапазона u16 только из-за ошибки округления, но для преобразований из f32 в 32-битные целые числа это не так очевидно. i32::MAX не представимы точно в f32, ближайший представима номер 47 от от i32::MAX . Поэтому, если у вас есть расчет, который математически должен привести к числу до i32::MAX , любая ошибка> = 1 ULP от нуля выведет вас за пределы допустимого диапазона. И становится намного хуже, если мы рассмотрим поплавки с более низкой точностью (двоичный код IEEE 75416 или нестандартный bfloat16).

Однако мы не говорим об умножении, мы говорим о приведениях

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

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

Я бы сказал, что несоответствие здесь оправдано обычной практикой и не было бы таким удивительным. Усечение и разделение целых чисел с помощью сдвигов, масок и приведений - эффективное использование приведения типов как формы побитового И плюс изменение размера - очень распространено и имеет долгую историю в системном программировании. Я делаю это как минимум несколько раз в неделю. Но за последние 30 с лишним лет я не могу припомнить, чтобы когда-либо ожидал получить разумный результат от преобразования NaN, Infinity или значения с плавающей запятой вне допустимого диапазона в целое число. (Каждый пример того, что я могу вспомнить, был ошибкой в ​​вычислении, которое произвело значение.) Поэтому я не думаю, что случаи целочисленного -> целочисленного преобразования и с плавающей запятой -> целочисленного преобразования должны рассматриваться одинаково. Тем не менее, я понимаю, что некоторые решения уже высечены в камне.

пожалуйста ... предложите разумную (не связанную с замораживанием) семантику без паники для преобразований типа float в int

Что ж, мое предложение:

  1. Не используйте глобальные переключатели компиляции, которые существенно изменяют семантику. (Я предполагаю, что -Zsaturating-float-castts является параметром командной строки или аналогичным ему.) Код, который зависит, скажем, от поведения насыщения, будет поврежден, если скомпилирован без него. Предположительно, код с разными ожиданиями нельзя смешивать в одном проекте. Должен быть какой-то локальный способ для вычисления желаемой семантики, возможно, что-то вроде этого предварительного RFC .
  2. Сделайте так, чтобы приведения as имели максимальную производительность по умолчанию, как и следовало ожидать от приведения.

    • Я думаю, что это должно быть сделано путем замораживания версий LLVM, которые его поддерживают, и любой другой семантики преобразования в версиях LLVM, которые этого не делают (например, усечение, насыщение и т. Д.). Я полагаю, что утверждение «замораживание может привести к утечке значений из чувствительной памяти» является чисто гипотетическим. (Или, если y = freeze(fptosi(x)) просто оставляет y без изменений, что приводит к утечке неинициализированной памяти, это можно исправить, сначала очистив y .)

    • Если as по умолчанию будет относительно медленным (например, из-за насыщения), предоставьте какой-либо способ получения максимальной производительности (например, метод - небезопасный, если необходимо, - использующий замораживание).

  1. Не используйте глобальные переключатели компиляции, которые существенно изменяют семантику. (Я предполагаю, что -Zsaturating-float-castts - это параметр командной строки или аналогичный.)

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

Для f32-> u16 может оказаться правдой, что вам нужна чрезвычайно большая ошибка округления, чтобы выпасть из диапазона u16 только из-за ошибки округления, но для преобразований из f32 в 32-битные целые числа это не так очевидно. i32 :: MAX не может быть точно представлен в f32, ближайшее представимое число отличается от i32 :: MAX на 47. Итак, если у вас есть расчет, который математически должен привести к числу до i32 :: MAX, любая ошибка> = 1 ULP от нуля выведет вас за пределы

Это становится немного не по теме, но предположим, что у вас есть этот гипотетический алгоритм, который должен математически производить f32 до 2 ^ 31-1 (но не должен _не_ давать 2 ^ 31 или выше, за исключением, возможно, ошибки округления). Кажется, это уже ошибочно.

  1. Я думаю, что ближайший представимый i32 на самом деле на 127 ниже i32 :: MAX, поэтому даже в идеальном мире без неточности с плавающей запятой алгоритм, который, как вы ожидаете, будет производить значения до 2 ^ 31-1, на самом деле может производить только (законный ) значения до 2 ^ 31-128. Возможно, это уже ошибка. Я не уверен, что имеет смысл говорить об ошибке, измеренной от 2 ^ 31-1, когда это число невозможно представить. Вам нужно будет отклониться на 64 от ближайшего представимого числа (с учетом округления), чтобы выйти за пределы. Конечно, это не так много в процентном отношении, когда вы приближаетесь к 2 ^ 32.
  2. Вы не должны ожидать различения значений, которые разнесены на 1 (т.е. 2 ^ 31-1, но не 2 ^ 31), когда ближайшие представимые значения разнесены на 128. Более того, только 3,5% i32s могут быть представлены как f32 (и <2% u32s). Вы не можете получить такой диапазон, имея при этом такую ​​точность с f32. Похоже, алгоритм использует не тот инструмент для работы.

Я полагаю, что любой практический алгоритм, который делает то, что вы описываете, каким-то образом будет тесно связан с целыми числами. Например, если вы конвертируете случайный i32 в f32 и обратно, он может потерпеть неудачу, если он выше i32 :: MAX-64. Но это сильно снижает вашу точность, и я не знаю, зачем вы так поступили. Практически любое вычисление i32 -> f32 -> i32, которое выводит полный диапазон i32, может быть выражено быстрее и точнее с помощью целочисленной математики, а если нет, то есть f64.

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

PS Поздравляю всех с Днем благодарения.

Чтобы быть ясным, я не думаю, что кто-то не согласен. Этот флаг когда-либо предлагался только как краткосрочный инструмент ...

В первую очередь я имел в виду предложение заменить -Zsaturation-float-cast на -Zunsaturation-float-cast. Даже если насыщенность становится значением по умолчанию, такие флаги, как -Zunsaturation-float-castts, кажутся плохими для совместимости, но если они также предназначены для временного использования, то ладно, неважно. :-)

В любом случае, я уверен, что все надеются, что я сказал достаточно по этому поводу, включая меня. Я знаю, что команда Rust традиционно стремилась предоставить несколько способов решения задач, чтобы люди могли делать свой собственный выбор между производительностью и безопасностью. Я поделился своей точкой зрения и верю, что вы, ребята, в конце концов найдете хорошее решение. Береги себя!

Я предположил, что -Zunsaturated-float-casts будет существовать только временно и в какой-то момент будет удален. То, что это вариант -Z (доступен только в Nightly), а не -C предполагает это, по крайней мере.

Как бы то ни было, насыщенность и UB - не единственные варианты. Другая возможность - изменить LLVM, добавив вариант fptosi который использует собственное поведение ЦП при переполнении, т.е. поведение при переполнении не будет переносимым для разных архитектур, но оно будет четко определено для любой данной архитектуры ( например, возврат 0x80000000 на x86), и он никогда не вернет яд или неинициализированную память. Даже если значение по умолчанию становится насыщенным, было бы неплохо иметь это в качестве опции. В конце концов, в то время как насыщающие приведения имеют внутренние накладные расходы на архитектурах, где они не являются поведением по умолчанию, «делать то, что делает ЦП», имеет накладные расходы только в том случае, если это препятствует некоторой конкретной оптимизации компилятора. Я не уверен, но подозреваю, что любые оптимизации, включенные путем обработки переполнения float-to-int как UB, являются нишевыми и неприменимы к большей части кода.

Тем не менее, одна проблема может возникнуть, если в архитектуре есть несколько инструкций типа float-to-int, которые возвращают разные значения при переполнении. В этом случае компилятор, выбирающий тот или иной вариант, повлияет на наблюдаемое поведение, что само по себе не является проблемой, но может стать таковым, если один fptosi будет дублирован, а две копии будут вести себя по-разному. Но я не уверен, существует ли такое расхождение на каких-либо популярных архитектурах. И та же проблема относится к другим оптимизациям с плавающей запятой, включая сжатие с плавающей запятой ...

const fn (miri) уже выбрал поведение с насыщенным приведением, начиная с Rust 1.26 (при условии, что мы хотим, чтобы результаты CTFE и RTFE были согласованными) (до 1.26 переполняющееся приведение во время компиляции возвращает 0)

const fn g(a: f32) -> i32 {
    a as i32
}

const Q: i32 = g(1e+12);

fn main() {
    println!("{}", Q); // always 2147483647
    println!("{}", g(1e+12)); // unspecified value, but always 2147483647 in miri
}

Miri / CTFE использует методы apfloat to_u128 / to_i128 для преобразования. Но я не уверен, является ли это стабильной гарантией - учитывая, в частности, что она, похоже, изменилась раньше (о чем мы не знали при реализации этого материала в Miri).

Я думаю, мы могли бы приспособить это к тому, что в итоге выберет кодогенератор. Но тот факт, что apfloat LLVM (прямым портом которого является версия Rust) использует насыщение, является хорошим индикатором того, что это своего рода «разумное значение по умолчанию».

Одним из решений наблюдаемого поведения может быть случайный выбор одного из доступных методов во время сборки компилятора или результирующего двоичного файла.
Затем есть функции типа a.saturating_cast::<i32>() для пользователей, которым требуется определенное поведение.

@ dns2utf8

Слово «случайно» противоречило бы усилиям по получению воспроизводимых сборок, и, если оно предсказуемо в рамках версии компилятора, вы знаете, что кто- то решит, что оно не изменится.

ИМО, что описал @comex (не темы IIRC, все старое снова новое), это следующий лучший вариант, если мы не хотим насыщения. Обратите внимание, что нам даже не нужны никакие изменения LLVM для проверки, мы можем использовать встроенный asm (на архитектурах, где такие инструкции существуют).

Тем не менее, одна проблема может возникнуть, если в архитектуре есть несколько инструкций типа float-to-int, которые возвращают разные значения при переполнении. В этом случае компилятор, выбирающий тот или иной вариант, повлияет на наблюдаемое поведение, что само по себе не является проблемой, но может стать таковым, если один fptosi будет дублирован, а две копии будут вести себя по-разному.

IMO такой недетерминизм лишил бы почти всех практических преимуществ по сравнению с freeze . Если мы сделаем это, мы должны выбрать одну инструкцию для каждой архитектуры и придерживаться ее, как для детерминизма, так и для того, чтобы программы действительно могли полагаться на поведение инструкции, когда это имеет для них смысл. Если это невозможно в какой-то архитектуре, мы могли бы вернуться к программной реализации (но, как вы говорите, это полностью гипотетически).

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

@rkruppe

[...] Что, кстати, было бы намного проще, чем изменить LLVM, чтобы добавить новые встроенные функции и понизить их для каждого бэкэнда.

Вдобавок LLVM не очень доволен встроенными функциями с целевой семантикой:

Однако, если вы хотите, чтобы приведения были четко определены, вам следует определить их поведение. «Делай что-нибудь быстро» на самом деле не является определением, и я не считаю, что мы должны давать независимые от цели конструкции, зависящие от цели поведение.

https://groups.google.com/forum/m/#!msg/llvm -dev / cgDFaBmCnDQ / CZAIMj4IBAA

Я собираюсь пометить # 10184 исключительно как T-lang: я думаю, что проблемы, которые нужно решить, есть семантические варианты того, что означает float as int

(то есть готовы ли мы позволить ему иметь семантику паники или нет, готовы ли мы позволить ему иметь недостаточную спецификацию на основе freeze или нет и т. д.)

это вопросы, которые лучше адресовать команде T-lang, а не T-компилятору, по крайней мере, для первоначального обсуждения, ИМО

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

Я предлагаю просто полностью запретить использование as для «float as int» и вместо этого полагаться на определенные методы округления. Рассуждение: as не с потерями для других типов.

Рассуждение: как не с потерями для других типов.

Это?

Основываясь на Rust Book, я могу предположить, что это без потерь только в определенных случаях (а именно в тех случаях, когда From<X> определено для типа Y), то есть вы можете преобразовать u8 в u32 используя From , но не наоборот.

Под «без потерь» я подразумеваю приведение значений, достаточно малых, чтобы уместиться. Пример: 1_u64 as u8 не с потерями, поэтому u8 as u64 as u8 не с потерями. Для чисел с плавающей запятой не существует простого определения «совпадений», поскольку 20000000000000000000000000000_u128 as f32 не является без потерь, а 20000001_u32 as f32 - нет, поэтому ни float as int и int as float являются без потерь.

256u64 as u8 с потерями.

Но <anything>_u8 as u64 as u8 - нет.

Я думаю, что потеря качества - это нормально и ожидается с приведением типов, и это не проблема. Усечение целых чисел с помощью приведения типов (например, u32 as u8 ) - это обычная операция с хорошо понятным значением, которое согласуется со всеми C-подобными языками, о которых я знаю (по крайней мере, на архитектурах, использующих представления двух дополнительных целочисленных значений, в основном все они в наши дни). Допустимые преобразования с плавающей запятой (т. Е. Когда неотъемлемая часть соответствует месту назначения) также имеют хорошо понятную и согласованную семантику. 1.6 as u32 с потерями, но все известные мне C-подобные языки согласны с тем, что результат должен быть 1. Оба эти случая вытекают из консенсуса между производителями оборудования относительно того, как эти преобразования должны работать, и соглашения в C -подобные языки с приведением типов должны быть высокопроизводительными операторами типа «я знаю, что делаю».

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

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

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

и у него нет никаких побочных эффектов, кроме создания мусорного значения

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

Допустимые преобразования с плавающей запятой (т. Е. Когда неотъемлемая часть соответствует месту назначения) также имеют хорошо понятную и согласованную семантику.

Существуют ли какие-либо варианты использования преобразований типа float в int без явного указания trunc() , round() , floor() или ceil() ? Текущая стратегия округления as - "undefined", поэтому as едва ли можно использовать для неокругленных чисел. Я считаю, что в большинстве случаев тот, кто пишет x as u32 действительно хочет x.round() as u32 .

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

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

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

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

Существуют ли какие-либо варианты использования преобразований типа float в int, не сопровождаемые явным trunc (), round (), floor () или ceil ()? Текущая стратегия округления as - "undefined", что делает ее практически непригодной для неокругленных чисел.

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

Я считаю, что в большинстве случаев тот, кто пишет x as u32 действительно хочет x.round() as u32 .

Я предполагаю, что это зависит от домена, но я думаю, что x.trunc() as u32 также довольно часто требуется.

Я согласен, но только если потери легко предсказуемы.

Я однозначно согласен. Например, не следует определять, станет ли 1.6 as u32 1 или 2.

https://doc.rust-lang.org/nightly/reference/expressions/operator-expr.html#type -cast-expressions

Преобразование числа с плавающей точкой в ​​целое число округляет число с плавающей запятой до нуля
ПРИМЕЧАНИЕ: в настоящее время это приведет к неопределенному поведению, если округленное значение не может быть представлено целевым целым типом. Это включает Inf и NaN. Это ошибка, и она будет исправлена.

Ссылка на заметку здесь.

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

Остается решить, как определить f as $Int в следующих случаях:

  • f.trunc() > $Int::MAX (включая положительную бесконечность)
  • f.trunc() < $Int::MIN (включая отрицательную бесконечность)
  • f.is_nan()

Один из вариантов, который уже реализован и доступен в Nightly с флагом компилятора -Z saturating-casts заключается в том, чтобы определить их для возврата соответственно: $Int::MAX , $Int::MIN и ноль. Но все же возможно выбрать другое поведение.

Я считаю, что поведение определенно должно быть детерминированным и возвращать некоторое целочисленное значение (например, а не панику), но точное значение не слишком важно, и пользователям, которым небезразличны эти случаи, лучше использовать методы преобразования, которые я отдельно предлагаю нам. добавить: https://internals.rust-lang.org/t/pre-rfc-add-explicitly- named-numeric-conversion-apis/11395

Я предполагаю, что это зависит от домена, но я думаю, что x.trunc() as u32 также довольно часто используется.

Верный. Как правило, x.anything() as u32 , скорее всего round() , но также может быть trunc() , floor() , ceil() . Просто x as u32 без указания конкретной процедуры округления, скорее всего, будет ошибкой.

Я считаю, что поведение определенно должно быть детерминированным и возвращать некоторое целочисленное значение (например, а не панику), но точное значение не слишком важно.

Лично меня устраивает даже значение undefined при условии, что оно не зависит ни от чего, кроме самого float, и, что наиболее важно, не раскрывает никакое несвязанное содержимое регистров и памяти.

Один из вариантов, который уже реализован и доступен в Nightly с флагом компилятора -Z saturating-casts состоит в том, чтобы определить их для возврата соответственно: $Int::MAX , $Int::MIN и ноль. Но все же возможно выбрать другое поведение.

Поведение, которое я ожидал бы получить для f.trunc() > $Int::MAX и f.trunc() < $Int::MIN такое же, как когда мнимое число с плавающей запятой преобразуется в целое число бесконечного размера, а затем возвращаются младшие значащие биты ( как при преобразовании целочисленных типов). Технически это были бы некоторые биты значимого, сдвинутые влево в зависимости от экспоненты (для положительных чисел отрицательные числа нуждаются в инверсии в соответствии с дополнением до двух).

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

Кажется, сложнее / произвольнее определить, во что преобразуется бесконечность и NaN.

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

https://github.com/WebAssembly/nontrapping-float-to-int-conversions/blob/master/proposals/nontrapping-float-to-int-conversion/Overview.md#design

@CryZe, значит, если я правильно прочитал, это соответствует -Z saturating-casts (и что Мири уже реализует)?

@RalfJung Верно.

Замечательно, я скопирую https://github.com/WebAssembly/testsuite/blob/master/conversions.wast (с заменой ловушек указанными результатами) в набор тестов Мири. :)

@RalfJung Пожалуйста, обновите

@sunfishcode, спасибо за обновление! Мне все равно нужно перевести тесты на Rust, так что мне еще многое придется заменить. ;)

Отличаются ли тесты _sat тестируемыми значениями? (РЕДАКТИРОВАТЬ: там есть комментарий, в котором говорится, что значения такие же.) Для насыщающих приведений Rust я взял многие из этих значений и добавил их в https://github.com/rust-lang/miri/pull/1321. Мне было лень делать это для всех ... но я думаю, это значит, что сейчас с обновленным файлом ничего менять не надо.

Для внутреннего UB ловушки на стороне wasm должны затем превратиться в тесты на сбой компиляции в Miri, я думаю.

Все входные значения одинаковы, с той лишь разницей, что операторы _sat имеют ожидаемые выходные значения на входах, где операторы перехвата имеют ожидаемые ловушки.

Тесты для Miri (а значит, и для движка Rust CTFE) были добавлены в https://github.com/rust-lang/miri/pull/1321. Я локально проверил, что rustc -Zmir-opt-level=0 -Zsaturating-float-casts также проходит тесты в этом файле.
Теперь я также реализовал неотмеченную встроенную функцию в Мири, см. Https://github.com/rust-lang/miri/pull/1325.

Я опубликовал https://github.com/rust-lang/rust/pull/71269#issuecomment -615537137, в котором документируется текущее состояние, как я его понял, и что PR также пытается стабилизировать поведение насыщающего флага -Z.

Учитывая длину этой темы, я думаю, что если люди почувствуют, что я что-то пропустил в этом комментарии, я бы направил комментарий в PR или, если он незначительный, не стесняйтесь пинговать меня в Zulip или Discord (simulacrum), и я могу исправьте ситуацию, чтобы избежать ненужного шума в цепочке PR.

Я ожидаю, что кто-то из языковой команды, скорее всего, скоро внесет предложение FCP по этому PR, и его объединение автоматически закроет эту проблему :)

Есть ли планы на проверенные конверсии? Что-то вроде fn i32::checked_from(f64) -> Result<i32, DoesntFit> ?

Вам нужно будет подумать, что должно вернуть i32::checked_from(4.5) .

Была ли эта страница полезной?
0 / 5 - 0 рейтинги