Rust: Оптимизация цикла LLVM может привести к сбою безопасных программ

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

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

enum Null {}

fn foo() -> Null { loop { } }

fn create_null() -> Null {
    let n = foo();

    let mut i = 0;
    while i < 100 { i += 1; }
    return n;
}

fn use_null(n: Null) -> ! {
    match n { }
}


fn main() {
    use_null(create_null());
}

https://play.rust-lang.org/?gist=1f99432e4f2dccdf7d7e&version=stable

Это основано на следующем примере LLVM, удаляющего петлю, о которой мне известно: https://github.com/simnalamburt/snippets/blob/12e73f45f3/rust/infinite.rs.
Похоже, что, поскольку C позволяет LLVM удалять бесконечные циклы, которые не имеют побочных эффектов, мы в конечном итоге выполняем match который должен активировать.

A-LLVM C-bug E-medium I-needs-decision I-unsound 💥 P-medium T-compiler WG-embedded

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

Если кто-то хочет сыграть в гольф с тестовым кодом:

pub fn main() {
   (|| loop {})()
}

С флагом -Z insert-sideeffect rustc, добавленным @sfanxiang в https://github.com/rust-lang/rust/pull/59546, он продолжает цикл :)

до:

main:
  ud2

после:

main:
.LBB0_1:
  jmp .LBB0_1

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

LLVM IR оптимизированного кода:

; Function Attrs: noreturn nounwind readnone uwtable
define internal void @_ZN4main20h5ec738167109b800UaaE() unnamed_addr #0 {
entry-block:
  unreachable
}

Такая оптимизация нарушает основное предположение, которое обычно должно выполняться для необитаемых типов: должно быть невозможно иметь значение этого типа.
rust-lang / rfcs # 1216 предлагает явную обработку таких типов в Rust. Это может быть эффективным для гарантии того, что LLVM никогда не будет их обрабатывать, и для внедрения соответствующего кода для обеспечения расхождения, когда это необходимо (IIUIC может быть достигнуто с помощью соответствующих атрибутов или внутренних вызовов).
Эта тема также недавно обсуждалась в списке рассылки LLVM: http://lists.llvm.org/pipermail/llvm-dev/2015-July/088095.html

сортировка: номинирован I

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

Способ предотвратить оптимизацию бесконечных циклов - это добавить внутрь них unsafe {asm!("" :::: "volatile")} . Это похоже на внутреннюю функцию llvm.noop.sideeffect , которая была предложена в списке рассылки LLVM, но может помешать некоторым оптимизациям.
Чтобы избежать потери производительности и по-прежнему гарантировать, что расходящиеся функции / циклы не будут оптимизированы, я считаю, что должно быть достаточно вставить пустой неоптимизируемый цикл (например, loop { unsafe { asm!("" :::: "volatile") } } ), если ненаселенные значения находятся в объем.
Если LLVM оптимизирует код, который должен расходиться до такой степени, что он больше не расходится, такие циклы гарантируют, что поток управления по-прежнему не может продолжаться.
В «удачном» случае, когда LLVM не может оптимизировать расходящийся код, такой цикл будет удален DCE.

Это связано с # 18785? Речь идет о бесконечной рекурсии для UB, но похоже, что основная причина может быть аналогичной: LLVM не считает, что остановка не является побочным эффектом, поэтому, если функция не имеет побочных эффектов, кроме отсутствия остановки, она с радостью оптимизирует это прочь.

@geofft

Это та же проблема.

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

: +1:

Сбой или, возможно, еще хуже, сердцебиение https://play.rust-lang.org/?gist=15a325a795244192bdce&version=stable

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

  1. цикл завершится ИЛИ
  2. цикл будет иметь побочные эффекты (операции ввода-вывода и т.д., я точно забыл, как это определено в C)

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

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

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

Цитата из обсуждения в списке рассылки LLVM:

 The implementation may assume that any thread will eventually do one of the following:
   - terminate
   - make a call to a library I/O function
   - access or modify a volatile object, or
   - perform a synchronization operation or an atomic operation

 [Note: This is intended to allow compiler transformations such as removal of empty loops, even
  when termination cannot be proven. — end note ]

@dotdash Цитируемый вами отрывок

Что касается ожидаемого поведения LLVM IR, существует некоторая путаница. https://llvm.org/bugs/show_bug.cgi?id=24078 показывает, что, похоже, нет точной и явной спецификации семантики бесконечных циклов в LLVM IR. Он соответствует семантике C ++, скорее всего, по историческим причинам и для удобства (мне удалось отследить только https://groups.google.com/forum/#!topic/llvm-dev/j2vlIECKkdE, который, по-видимому, относится к времени когда бесконечные циклы не были оптимизированы, за некоторое время до того, как спецификации C / C ++ были обновлены, чтобы разрешить это).

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

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

Это проблема надежности? Если это так, мы должны пометить его как таковой.

Да, следуя примеру @ ranma42 , этот способ показывает, как легко обходить проверку границ массива. игровая площадка

@bluss

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

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

  • Подождите, пока LLVM предоставит решение.
  • Вводите операторы asm без использования операций везде, где может быть бесконечный цикл или бесконечная рекурсия (# 18785).

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

Подождите, пока LLVM предоставит решение.

Какая ошибка LLVM отслеживает эту проблему?

примечание: while true {} демонстрирует такое поведение . Может быть, линт должен быть обновлен до ошибки по умолчанию и получить примечание о том, что в настоящее время это может проявлять неопределенное поведение?

Также обратите внимание, что это недопустимо для C. LLVM этот аргумент означает, что в clang есть ошибка.

void foo() { while (1) { } }

void create_null() {
        foo();

        int i = 0;
        while (i < 100) { i += 1; }
}

__attribute__((noreturn))
void use_null() {
        __builtin_unreachable();
}


int main() {
        create_null();
        use_null();
}

Это вылетает при оптимизации; это недопустимое поведение по стандарту C11:

An iteration statement whose controlling expression is not a constant
expression, [note 156] that performs no  input/output  operations,
does  not  access  volatile  objects,  and  performs  no synchronization or
atomic operations in its body, controlling expression, or (in the case of
a for statement) its expression-3, may be   assumed   by   the
implementation to terminate. [note 157]

156: An omitted controlling expression is replaced by a nonzero constant,
     which is a constant expression.
157: This  is  intended  to  allow  compiler  transformations  such  as
     removal  of  empty  loops  even  when termination cannot be proven. 

Обратите внимание на «чье управляющее выражение не является постоянным выражением» - while (1) { } , 1 является постоянным выражением, поэтому его нельзя удалить .

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

@ubsan

Вы нашли отчет об ошибке в bugzilla LLVM или заполнили его? Кажется, что в C ++ бесконечные циклы, которые _can_ никогда не завершаются, являются неопределенным поведением, но в C они являются определенным поведением (в некоторых случаях они могут быть безопасно удалены, а в других - нет).

@gnzlbg Сейчас я регистрирую ошибку.

https://llvm.org/bugs/show_bug.cgi?id=31217

Повторяюсь из # 42009: эта ошибка может, при некоторых обстоятельствах, вызвать выброс вызываемой извне функции, не содержащей вообще никаких машинных инструкций. Такого никогда не должно быть. Если LLVM приходит к выводу, что pub fn никогда не может быть вызвано правильным кодом, он должен выдать как минимум команду прерывания в качестве тела этой функции.

Об этом сегодня говорилось в этом сообщении в блоге:

С более простым воспроизведением: https://play.rust-lang.org/?gist=e622f8a672fbc57ecc63eb4450d2fc0a&version=stable

Ошибка LLVM для этого - https://bugs.llvm.org/show_bug.cgi?id=965 (открыта в 2006 году).

@zackw LLVM имеет для этого флаг: TrapUnreachable . Я не тестировал это, но похоже, что добавление Options.TrapUnreachable = true; к LLVMRustCreateTargetMachine должно решить вашу проблему. Вероятно, это имеет достаточно низкую стоимость, чтобы это можно было сделать по умолчанию, хотя я не проводил никаких измерений.

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

Похоже, есть патч: https://reviews.llvm.org/D38336

@sunfishcode , похоже, что ваш патч LLVM на https://reviews.llvm.org/D38336 был "принят" 3 октября, не могли бы вы рассказать, что это означает в отношении процесса выпуска LLVM? Каков следующий шаг после принятия, и есть ли у вас представление о том, какой будущий выпуск LLVM будет содержать этот патч?

Я разговаривал с некоторыми людьми в автономном режиме, которые предложили нам создать ветку llvmdev. Тема здесь:

http://lists.llvm.org/pipermail/llvm-dev/2017-October/118558.html

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

Спасибо за обновление и большое спасибо за ваши усилия!

Обратите внимание, что https://reviews.llvm.org/rL317729 приземлился в LLVM. Планируется, что этот патч будет иметь последующий патч, который заставляет бесконечные циклы демонстрировать определенное поведение по умолчанию, поэтому AFAICT все, что нам нужно сделать, это подождать, и в конечном итоге это будет решено для нас вверх по течению.

@zackw Я создал # 45920, чтобы исправить проблему функций, не содержащих кода.

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

Я не мог воспроизвести это прямо сейчас: https://play.rust-lang.org/?gist=529bd5ab326f7b627e559f64d514312f&version=stable

@jsgf Еще

@kennytm Уупс , неважно.

Обратите внимание, что https://reviews.llvm.org/rL317729 приземлился в LLVM. Планируется, что этот патч будет иметь последующий патч, который заставляет бесконечные циклы демонстрировать определенное поведение по умолчанию, поэтому AFAICT все, что нам нужно сделать, это подождать, и в конечном итоге это будет решено для нас вверх по течению.

С момента этого комментария прошло несколько месяцев. Кто-нибудь знает, случился ли следующий патч или все еще будет?

В качестве альтернативы кажется, что внутренняя функция llvm.sideeffect существует в используемой нами версии LLVM: можем ли мы исправить это сами, переведя бесконечные циклы Rust в циклы LLVM, содержащие внутренний побочный эффект?

Как видно на https://github.com/rust-lang/rust/issues/38136 и https://github.com/rust-lang/rust/issues/54214 , это особенно плохо с предстоящим panic_implementation , в качестве логической реализации этого будет loop {} , и это сделает все вхождения panic! UB без кода unsafe . Что… может быть, хуже того, что могло случиться.

Просто столкнулся с этой проблемой в другом свете. Вот пример:

pub struct Container<'f> {
    string: &'f str,
    num: usize,
}

impl<'f> From<&'f str> for Container<'f> {
    #[inline(always)]
    fn from(string: &'f str) -> Container<'f> {
        Container::from(string)
    }
}

fn main() {
    let x = Container::from("hello");
    println!("{} {}", x.string, x.num);

    let y = Container::from("hi");
    println!("{} {}", y.string, y.num);

    let z = Container::from("hello");
    println!("{} {}", z.string, z.num);
}

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

@SergioBenitez эта программа не выполняет segfault, она завершается переполнением стека (вам нужно запустить ее в режиме отладки). Это правильное поведение, поскольку ваша программа просто повторяется бесконечно, требуя бесконечного количества пространства стека, которое в какой-то момент может превысить доступное пространство стека. Минимальный рабочий пример .

В сборках выпуска LLVM может предполагать, что у вас нет бесконечной рекурсии, и оптимизирует это прочь ( mwe ). Это не имеет ничего общего с циклами AFAICT, а скорее с https://stackoverflow.com/a/5905171/1422197

@gnzlbg Извините, но вы не правы.

Программа выходит из строя в режиме выпуска. В этом весь смысл; что оптимизация приводит к ненадлежащему поведению (семантика LLVM и Rust здесь не согласуется), что я могу написать и скомпилировать безопасную программу на Rust с помощью rustc, которая позволяет мне использовать неинициализированную память, проверять произвольную память и произвольно преобразовывать типы, нарушая семантика языка. Это то же самое, что показано в этой ветке. Обратите внимание, что исходная программа также не выполняет segfault в режиме отладки.

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

@gnzlbg Ну, немного изменив ваш подход к оптимизации, отказавшись от бесконечной рекурсии ( здесь ), он действительно генерирует неинициализированное значение NonZeroUsize (которое оказывается… 0, следовательно, недопустимое значение).

И это то, что @SergioBenitez также сделал со своим примером, за исключением того, что он использует указатели и, таким образом, генерирует segfault.

Согласны ли мы с тем, что программа

Если это так, я не могу найти никаких loop s в примере @SergioBenitez , поэтому я не знаю, как эта проблема будет применима к нему (в loop s). Если я ошибаюсь, укажите мне loop в вашем примере.

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

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

@rkruppe Моя программа выходит из

@gnzlbg Программа _не_ переполняет стек в режиме выпуска. В режиме выпуска программа не выполняет никаких вызовов функций; стек помещается в стек конечное число раз исключительно для выделения локальных переменных.

Программа не переполняется в режиме выпуска.

Так? Единственное, что имеет значение, это то, что программа-пример, которая по сути представляет собой fn foo() { foo() } , имеет бесконечную рекурсию, что не разрешено LLVM.

Единственное, что имеет значение, это то, что программа-пример, которая в основном представляет собой fn foo () {foo ()}, имеет бесконечную рекурсию, что не разрешено LLVM.

Я не знаю, почему вы так говорите, будто это что-то решает. LLVM рассматривает бесконечную рекурсию и циклы UB и оптимизирует соответственно, но при этом он безопасен в Rust, вот и весь смысл этой проблемы!

Автор https://reviews.llvm.org/rL317729 здесь, подтверждающий, что я еще не внедрил следующий патч.

Вы можете вставить вызовы @llvm.sideeffect сегодня, чтобы гарантировать, что циклы не будут оптимизированы. Это может отключить некоторые оптимизации, но теоретически не слишком много, поскольку основные оптимизации были обучены тому, как это понимать. Если поместить вызовы @llvm.sideeffect во все циклы или вещи, которые могут превратиться в циклы (рекурсия, раскрутка, встроенный asm и т. Д.?), Этого теоретически достаточно, чтобы решить проблему.

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

Есть небольшая разница, но я не уверен, материальная она или нет.

Рекурсия

#[allow(unconditional_recursion)]
#[inline(never)]
pub fn via_recursion<T>() -> T {
    via_recursion()
}

fn main() {
    let a: String = via_recursion();
}
define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* <strong i="9">@rust_eh_personality</strong> {
_ZN4core3ptr13drop_in_place17h95538e539a6968d0E.exit:
  ret void
}

Петля

#[inline(never)]
pub fn via_loop<T>() -> T {
    loop {}
}

fn main() {
    let b: String = via_loop();
}
define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 {
start:
  unreachable
}

Мета

Rust 1.29.1, компиляция в режиме релиза, просмотр LLVM IR.

Я не думаю, что мы можем вообще обнаружить рекурсию (объекты-признаки, C FFI и т. Д.), Поэтому нам пришлось бы использовать llvm.sideeffect почти на каждом сайте вызова, если мы не докажем, что вызов сайт не будет рекурсивно. Доказательство отсутствия рекурсий для случаев, в которых это может быть доказано, требует межпроцедурного анализа, за исключением самых тривиальных программ, таких как fn main() { main() } . Было бы неплохо узнать, как повлияет внедрение этого исправления, и есть ли альтернативные решения этой проблемы.

@gnzlbg Это правда, хотя вы можете поместить эффекты @ llvm.sideeffects в записи функций, а не в сайты вызовов.

Как ни странно, я не могу воспроизвести SEGFAULT в тестовом примере @SergioBenitez локально.

Кроме того, при переполнении стека не должно быть другого сообщения об ошибке? Я думал, у нас есть код для вывода «Стек переполнен» или около того?

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


@sunfishcode

Это правда, хотя вы можете поместить эффекты @ llvm.sideeffects в записи функций, а не в сайты вызовов.

Трудно сказать, каким был бы лучший путь вперед, не зная точно, какие оптимизации предотвращает llvm.sideeffects . Стоит ли пытаться сгенерировать как можно меньше @llvm.sideeffects ? Если нет, то включение его в каждый вызов функции может быть самым простым делом. В противном случае IIUC, требуется ли @llvm.sideeffect зависит от того, что делает сайт вызова:

trait Foo {
    fn foo(&self) { self.bar() }
    fn bar(&self);
}

struct A;
impl Foo for A {
    fn bar(&self) {} // not recursive
}
struct B;
impl Foo for B {
    fn bar(&self) { self.foo() } // recursive
}

fn main() {
    let a = A;
    a.bar(); // Ok - no @llvm.sideeffect needed anywhere
    let b = B;
    b.bar(); // We need @llvm.sideeffect on this call site
    let c: &[&dyn Foo] = &[&a, &b];
    for i in c {
        i.bar(); // We need @lvm.sideeffect here too
    }
}

AFAICT, мы должны поместить @llvm.sideeffect внутри функций, чтобы предотвратить их удаление, так что даже если бы эти «оптимизации» того стоили, я не думаю, что они просты в использовании с текущей моделью. Даже если бы это было так, эти оптимизации полагались бы на возможность доказать отсутствие рекурсии.

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

Конечно, но в режиме отладки LLVM не выполняет оптимизацию цикла, так что проблем нет.

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

Если программа переполняется в режиме отладки, это не должно давать лицензии LLVM на создание UB.

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

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

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

Это происходит с ошибками, но я не знаю почему.

LLVM удаляет бесконечную рекурсию, поэтому, как упомянуто выше @SergioBenitez , программа затем переходит к:

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

Часть программы, которая это делает, такова:

let x = Container::from("hello");  // invalid reference created here
println!("{} {}", x.string, x.num);  // invalid reference dereferenced here

где Container::from запускает бесконечную рекурсию, которая, по заключению LLVM, никогда не может произойти, и заменяется некоторым случайным значением, которое затем разыменовывается. Вы можете увидеть один из множества способов неправильной оптимизации здесь: https://rust.godbolt.org/z/P7Snex На игровой площадке (https://play.rust-lang.org/?gist=f00d41cc189f9f6897d429350f3781ec&version=stable&mode = release & edition = 2015) из-за этой оптимизации при выпуске отладочных сборок возникает другая паника, но UB - это UB - это UB.

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

У меня создалось впечатление, что вы утверждали выше, что это не та же ошибка, что и проблема с бесконечным циклом. Значит, я неправильно прочитал ваши сообщения. Извините за путаницу.

Итак, похоже, что следующим хорошим шагом будет добавить немного llvm.sideffect в генерируемый нами IR и провести некоторые тесты?

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

Кстати, это не совсем верно - петля с константой условно (например, while (true) { /* ... */ } ) явно разрешено стандартом, даже если она не содержит каких - либо побочных эффектов. В C ++ все по-другому. LLVM здесь неправильно реализует стандарт C.

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

Поведение незавершенных программ на Rust всегда определяется, в то время как поведение незавершенных программ LLVM-IR определяется только при соблюдении определенных условий.

Я думал, что эта проблема связана с исправлением реализации Rust для бесконечных циклов, так что поведение сгенерированного LLVM-IR становится определенным, и для этого @llvm.sideeffect звучит как довольно хорошее решение.

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

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

  • Что касается решения, мы переходим от применения барьера оптимизации ( @llvm.sideeffect ) исключительно к непрерывным циклам, чтобы применить его к каждой отдельной функции Rust.

  • по значению полезны бесконечные loop s, потому что программа никогда не завершается. Для бесконечной рекурсии, завершится ли программа, зависит от уровня оптимизации (например, преобразует ли LLVM рекурсию в цикл или нет), а когда и как программа завершится, зависит от платформы (размер стека, защищенная защитная страница и т. Д.). Исправление обоих необходимо, чтобы реализация Rust звучала правильно, но в случае бесконечной рекурсии, если пользователь намеревается, что их программа будет рекурсивной вечно, правильная реализация все равно будет «неправильной» в том смысле, что она не всегда будет повторяться вечно.

Что касается решения, мы переходим от применения барьера оптимизации (@ llvm.sideeffect) исключительно к непрерывным циклам, чтобы применить его к каждой отдельной функции Rust.

Анализ, необходимый для того, чтобы показать, что тело цикла действительно имеет побочные эффекты (не только потенциально , как при вызовах внешних функций) и, следовательно, не требует вставки llvm.sideeffect довольно сложен, вероятно, примерно в том же порядке величина, показывающая то же самое для функции, которая может быть частью бесконечной рекурсии. Доказать, что цикл завершается, также сложно без предварительной оптимизации, поскольку в большинстве циклов Rust используются итераторы. Так что я думаю, что мы все равно добавим llvm.sideeffect в подавляющее большинство циклов. По общему признанию, есть довольно много функций, которые не содержат циклов, но мне это все равно не кажется качественной разницей.

Если я правильно понимаю проблему, чтобы исправить случай бесконечного цикла, должно быть достаточно вставить llvm.sideeffect в loop { ... } и while <compile-time constant true> { ... } где тело цикла не содержит break выражения. Это отражает разницу между семантикой C ++ и семантикой Rust для бесконечных циклов: в Rust, в отличие от C ++, компилятору не разрешается предполагать, что цикл завершается, когда во время компиляции становится известно, что это не _не _. (Я не уверен, насколько нам нужно беспокоиться о правильности работы с петлями, из-за которых тело может паниковать, но это всегда можно исправить позже.)

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

@zackw

Если я правильно понимаю проблему, чтобы исправить случай с бесконечным циклом, должно быть достаточно вставить llvm.sideeffect в цикл {...} и while{...}, где тело цикла не содержит выражений прерывания.

Я не думаю, что это так просто, например, loop { if false { break; } } - это бесконечный цикл, содержащий выражение break , но нам нужно вставить @llvm.sideeffect чтобы llvm не удалил его. AFAICT, мы должны вставить @llvm.sideeffect если мы не сможем доказать, что цикл всегда завершается.

@gnzlbg

loop { if false { break; } } - это бесконечный цикл, содержащий выражение прерывания, но нам нужно вставить @llvm.sideeffect чтобы llvm не удалил его.

Хм, да, хлопотно. Но мы не должны быть идеальными, просто консервативно правильными. Петля вроде

while spinlock.load(Ordering::SeqCst) != 0 {}

(из документации std::sync::atomic ) было бы легко увидеть, что @llvm.sideeffect не требуется, поскольку условие управления не является постоянным (и операция атомарной загрузки лучше считаться побочным эффектом для целей LLVM , или у нас есть большие проблемы) Вид конечного цикла, который может генерировать программный генератор,

loop {
    if /* runtime-variable condition */ { break }
    /* more stuff */
}

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

loop {
    if /* provably false at compile time */ { break }
}

?

Я думал, что эта проблема связана с исправлением реализации Rust для бесконечных циклов, так что поведение сгенерированного LLVM-IR становится определенным, и для этого @ llvm.sideeffect звучит как довольно хорошее решение.

Справедливо. Однако, как вы сказали, проблема (несоответствие между семантикой Rust и семантикой LLVM) на самом деле связана с незавершением, а не с циклами. Так что я думаю, что мы должны здесь отслеживать.

@zackw

Если я правильно понимаю проблему, чтобы исправить случай с бесконечным циклом, должно быть достаточно вставить llvm.sideeffect в цикл {...} и while{...}, где тело цикла не содержит выражений прерывания. Это фиксирует разницу между семантикой C ++ и семантикой Rust для бесконечных циклов: в Rust, в отличие от C ++, компилятору не разрешается предполагать, что цикл завершается, если во время компиляции известно, что это не так. (Я не уверен, насколько нам нужно беспокоиться о правильности работы с петлями, из-за которых тело может паниковать, но это всегда можно исправить позже.)

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

Так, например

while test_fermats_last_theorem_on_some_random_number() { }

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

@zackw

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

Это не только if /*compile-time condition */ . Это затрагивает весь поток управления ( while , match , for , ...), а также условия выполнения.

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

Рассматривать:

fn foo(x: bool) { loop { if x { break; } } }

где x - условие времени выполнения. Если мы не испустим здесь @llvm.sideeffect , тогда, если пользователь где-то напишет foo(false) , foo может быть встроен и с постоянным распространением и устранением мертвого кода цикл будет оптимизирован до бесконечный цикл без побочных эффектов, что приводит к неправильной оптимизации.

Если это имеет смысл, одно преобразование, которое LLVM может выполнить, - это замена foo на foo_opt :

fn foo_opt(x: bool) { if x { foo(true) } else { foo(false) } }

где обе ветви оптимизируются независимо, а вторая ветвь будет оптимизирована неправильно, если мы не будем использовать @llvm.sideeffect .

То есть, чтобы можно было опустить @llvm.sideeffect , нам нужно доказать, что LLVM не может неправильно оптимизировать этот цикл ни при каких обстоятельствах. Единственный способ доказать это - либо доказать, что цикл всегда завершается, либо доказать, что, если он не завершается, он безусловно выполняет одну из вещей, предотвращающих неправильную оптимизацию. Даже тогда оптимизации, такие как разделение / отслаивание цикла, могут преобразовать один цикл в серию циклов, и для одного из них будет достаточно, чтобы не было @llvm.sideeffect чтобы произошла неправильная оптимизация.

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

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

С другой стороны, rustc может сделать это, только добавив @llvm.sideeffect , что потенциально окажет большее влияние на оптимизацию, чем «просто» отключение оптимизаций, которые неуместно используют прерывание. И rustc нужно будет встроить новый код, чтобы попытаться обнаружить (не) завершение циклов.

Поэтому я думаю, что дальнейший путь будет следующим:

  1. Добавьте @llvm.sideeffect в каждый цикл и вызов функции, чтобы устранить проблему.
  2. Исправьте LLVM, чтобы не выполнять неправильную оптимизацию в непрерывных циклах, и удалите @llvm.sideeffects

Что Вы думаете об этом? Я надеюсь, что влияние шага 1 на производительность не будет слишком ужасным, даже если оно исчезнет после реализации 2…

@Ekleog , о чем может быть второй патч @sunfishcode : https://lists.llvm.org/pipermail/llvm-dev/2017-October/118595.html

часть предложения атрибута функции состоит в том, чтобы
изменить семантику по умолчанию LLVM IR, чтобы определить поведение на
бесконечные циклы, а затем добавьте атрибут, выбирая в потенциал-UB. Так
если мы это сделаем, то роль @ llvm.sideeffect станет немного
тонкий - это был бы способ интерфейса для такого языка, как C, чтобы выбрать
в потенциальный UB для функции, но затем откажитесь от индивидуальных
циклы в этой функции.

Честно говоря, разработчики компиляторов не подходят к этой теме с точки зрения «Я собираюсь написать оптимизацию, которая докажет, что циклы не завершаются, чтобы я мог педантично оптимизировать их!» Вместо этого предположение, что циклы либо завершатся, либо будут иметь побочные эффекты, естественным образом возникает в некоторых распространенных алгоритмах компилятора. Это не просто поправка к существующему коду; это потребует значительного количества новой сложности.

Рассмотрим следующий алгоритм проверки, "не имеет ли тело функции побочных эффектов": если какая-либо инструкция в теле имеет потенциальные побочные эффекты, то тело функции может иметь побочные эффекты. Красиво и просто. Позже удаляются вызовы функций «без побочных эффектов». Круто. За исключением того, что инструкции ветвления считаются не имеющими побочных эффектов, поэтому функция, содержащая только ветви, будет казаться не имеющей побочных эффектов, даже если она может содержать бесконечный цикл. Ой.

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


Возможный компромисс может заключаться в использовании rustc insert @ llvm.sideeffect, когда пользователь буквально пишет пустой loop { } (или аналогичный) или безусловную рекурсию (которая уже имеет линт). Этот компромисс позволил бы людям, которые действительно намереваются создать бесконечный безрезультатный цикл вращения, получить его, избегая при этом никаких накладных расходов для всех остальных. Конечно, этот компромисс не сделает невозможным сбой безопасного кода, но, скорее всего, снизит вероятность того, что это произойдет случайно, и кажется, что это должно быть легко реализовать.

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

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

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

Есть веская причина, по которой «непрекращение» обычно считается эффектом, когда вы начинаете смотреть на вещи формально. (Haskell не является чистым, у него есть два эффекта: прерывание и исключения.)

Возможный компромисс может заключаться в использовании rustc insert @ llvm.sideeffect, когда пользователь буквально пишет пустой цикл {} (или аналогичный) или безусловную рекурсию (в которой уже есть линт). Этот компромисс позволил бы людям, которые действительно намереваются создать бесконечный безрезультатный цикл вращения, получить его, избегая при этом никаких накладных расходов для всех остальных. Конечно, этот компромисс не сделает невозможным сбой безопасного кода, но, скорее всего, снизит вероятность того, что это произойдет случайно, и кажется, что это должно быть легко реализовать.

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


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

Учитывая ограничения, с которыми мы работаем (а именно, LLVM), мы должны начать с добавления llvm.sideeffect в достаточном количестве мест, чтобы каждое расходящееся выполнение гарантированно «выполняло» бесконечно много из них. Затем мы достигли разумного (т.е. правильного и правильного) базового уровня и можем говорить об улучшениях путем удаления этих аннотаций, когда мы можем гарантировать, что они не нужны.

Чтобы уточнить мою точку зрения, я думаю, что это здоровый ящик Rust с pick_a_number_greater_2 возвращающим (недетерминированно) какой-то большой int:

fn test_fermats_last_theorem() -> bool {
  let x = pick_a_number_greater_2();
  let y = pick_a_number_greater_2();
  let z = pick_a_number_greater_2();
  let n = pick_a_number_greater_2();
  // x^n + y^n = z^n is impossible for n > 2
  pow(x, n) + pow(y, n) != pow(z, n)
}

pub fn diverge() -> ! {
  while test_fermats_last_theorem() { }
  // This code is unreachable, as proven by Andrew Wiles
  unsafe { mem::transmute(()) }
}

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

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

На практике fn foo() { foo() } всегда завершается из-за исчерпания ресурсов, но поскольку абстрактная машина Rust имеет бесконечно большой стековый фрейм (AFAIK), допустимо преобразовать этот код в fn foo() { loop {} } который никогда не будет прекратиться (или намного позже, когда Вселенная замерзнет). Должно ли это преобразование быть действительным? Я бы сказал да, поскольку в противном случае мы не сможем выполнить оптимизацию хвостового вызова, если не сможем доказать завершение, что было бы неудачно.

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

Я абсолютно согласен, что нам нужно исправить эту проблему со стабильностью навсегда. Тем не менее, мы должны помнить о возможности того, что «добавление llvm.sideeffect везде, где мы не можем доказать, что это не нужно» может ухудшить качество кода программ, которые сегодня скомпилированы правильно. Хотя такие опасения в конечном итоге перевешиваются необходимостью иметь надежный компилятор, было бы разумно действовать таким образом, чтобы немного откладывать правильное исправление в обмен на избежание снижения производительности и улучшение качества жизни среднего программиста на Rust в среднем. время. Я предлагаю:

  • Как и в случае с другими исправлениями, потенциально снижающими производительность для давних ошибок надежности (# 10184), мы должны реализовать исправление за флагом -Z, чтобы иметь возможность оценить влияние производительности на исходные кодовые базы.
  • Если влияние окажется незначительным, отлично, мы можем просто включить исправление по умолчанию.
  • Но если есть реальная регрессия, мы можем передать эти данные специалистам LLVM и попытаться сначала улучшить LLVM (или мы могли бы съесть регресс и исправить его позже, но в любом случае мы примем осознанное решение)
  • Если мы решим не включать исправление по умолчанию из-за регрессий, мы можем, по крайней мере, продолжить добавление llvm.sideeffect к синтаксически пустым циклам: они довольно распространены, и их неправильная компиляция привела к тому, что многие люди тратят очень мало часов на отладку странных проблем (# 38136, # 47537, # 54214 и, конечно же, есть и другие), поэтому, хотя это смягчение не имеет отношения к ошибке надежности, оно принесет ощутимую пользу разработчикам, пока мы будем исправлять неполадки в надлежащем Исправлена ​​ошибка.

Правда, об этой перспективе говорит тот факт, что этот вопрос стоит годами. Если бы это был свежий регресс, я был бы более открыт для более быстрого исправления или отмены PR, который его привел.

Между тем, следует ли об этом упоминать в https://doc.rust-lang.org/beta/reference/behavior-considered-undefined.html, пока этот вопрос открыт?

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

std::hint::reachable_unchecked ?

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

Если кто-то хочет сыграть в гольф с тестовым кодом:

fn main() {
    (|| loop {})()
}

`` ''
$ cargo run - выпуск
Нелегальная инструкция (дамп ядра)

Если кто-то хочет сыграть в гольф с тестовым кодом:

pub fn main() {
   (|| loop {})()
}

С флагом -Z insert-sideeffect rustc, добавленным @sfanxiang в https://github.com/rust-lang/rust/pull/59546, он продолжает цикл :)

до:

main:
  ud2

после:

main:
.LBB0_1:
  jmp .LBB0_1

Кстати, отслеживание ошибок LLVM - это https://bugs.llvm.org/show_bug.cgi?id=965 , которое я еще не видел в этой теме.

которые я еще не видел в этой теме.

https://github.com/rust-lang/rust/issues/28728#issuecomment -331460667 и https://github.com/rust-lang/rust/issues/28728#issuecomment -263956134

@RalfJung Можете ли вы обновить гиперссылку https://github.com/simnalamburt/snippets/blob/master/rust/src/bin/infinite.rs в описании проблемы на https://github.com/simnalamburt/snippets/blob /12e73f45f3/rust/infinite.rs это? Бывшая гиперссылка долго не работала, так как в ней не было постоянной ссылки. Благодаря! 😛

@simnalamburt готово, спасибо!

Повышение уровня MIR opt позволяет избежать неправильной оптимизации в следующем случае:

pub fn main() {
   (|| loop {})()
}

--emit=llvm-ir -C opt-level=1

define void @_ZN7example4main17hf7943ea78b0ea0b0E() unnamed_addr #0 !dbg !6 {
  unreachable
}

--emit=llvm-ir -C opt-level=1 -Z mir-opt-level=2

define void @_ZN7example4main17hf7943ea78b0ea0b0E() unnamed_addr #0 !dbg !6 {
  br label %bb1, !dbg !10

bb1:                                              ; preds = %bb1, %start
  br label %bb1, !dbg !11
}

https://godbolt.org/z/N7VHnj

rustc 1.45.0-nightly (5fd2f06e9 2020-05-31)

pub fn oops() {
   (|| loop {})() 
}

pub fn main() {
   oops()
}

Это помогло в этом частном случае, но не решило проблему в целом. https://godbolt.org/z/5hv87d

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

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

Что-то меняется со стороны LLVM: есть предложение добавить атрибут уровня функции для контроля гарантий выполнения. https://reviews.llvm.org/D85393

Я не уверен, почему все (здесь и в потоках LLVM), кажется, подчеркивают пункт о продвижении вперед.

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

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

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

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

Нельзя удалять следующий вызов функции, даже если результат не используется:

fn sideeffect() -> u32 {
  println!("Hello!");
  42
}

fn main() {
  let _ = sideffect(); // May not be removed.
}

Это верно для любого побочного эффекта и остается верным, когда вы заменяете print на loop {} .

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

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

Как в примере в потоке LLVM.

x = y % 42;
if y < 0 return 0;
...

Конечные свойства разделения не имеют ничего общего с переупорядочением. Современные процессоры будут пытаться выполнять разделение, сравнение, прогнозирование ветвления и предварительную выборку успешной ветвления параллельно. Таким образом, вам не гарантируется наблюдение за завершением деления в то время, когда вы наблюдаете возвращение 0 , если y отрицательно. (Под "наблюдать" здесь я имею в виду измерение с помощью осциллометра, в котором находится процессор, а не программой)

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

if y < 0 return 0;
x = y % 42;
...

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

Чистые петли ничем не отличаются.


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

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

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

Допустимые наблюдения всегда определяются на уровне источника. Спецификация языка определяет «абстрактную машину», которая описывает (в идеале с кропотливыми математическими подробностями), каково допустимое наблюдаемое поведение программы. В этом документе не говорится об оптимизации.

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

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

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

Для конкретности, если ваш пример деления был изменен на

x = 42 % y;
if y <= 0 { return 0; }

то я надеюсь, вы согласитесь, что условное _ нельзя_ поднять выше деления, потому что это изменит наблюдаемое поведение, когда y равно нулю (от сбоя до возврата нуля).

Таким же образом в

x = if y == 0 { loop {} } else { y % 42 };
if y < 0 { return 0; }

абстрактная машина Rust позволяет это переписать как

if y == 0 { loop {} }
else if y < 0 { return 0; }
x = y % 42;

но нельзя отбрасывать первое условие и цикл.

Ральф, я не делаю вид, что знаю половину того, что ты делаешь, и не хочу вводить новые значения. Я полностью согласен с определением того, что такое корректность (порядок выполнения должен соответствовать порядку программы). Я только думал, что «когда» наблюдаемое незавершение было частью этого, например: если вы не наблюдаете за результатом цикла, у вас нет свидетеля его завершения (поэтому не можете утверждать его неправильность) . Мне нужно вернуться к модели исполнения.

Спасибо за терпение

@zackw Спасибо. Это другой код, который, конечно же, приведет к другой оптимизации.

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

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

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

Другой пример:

fn main() {
  loop {}
  println!("Hello");
}

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

@RalfJung, все в порядке, теперь я понял. Моя первоначальная проблема заключалась в том, какую роль здесь играет «гарантия продвижения вперед». Оптимизация возможна полностью из зависимости от данных. Моя ошибка заключалась в том, что на самом деле зависимость данных не является частью программного порядка: это буквально выражения, полностью упорядоченные в соответствии с семантикой языка. Если порядок программы является полным, то без гарантии продвижения вперед (что мы можем повторить как «любой подпуть порядка программы конечен») мы можем переупорядочить (в порядке выполнения) только выражения, которые мы можем _ подтвердить_ как завершающие (и сохранение некоторых других свойств, таких как наблюдаемость действий синхронизации, вызовов ОС, ввода-вывода и т. д.).

Мне нужно немного подумать об этом, но я думаю, что могу понять причину, по которой мы можем «притвориться», что деление произошло в примере с x = y % 42 , даже если оно действительно не выполняется для некоторых входных данных, но почему то же самое не относится к произвольным циклам. Я имею в виду тонкости соответствия общего (программного) порядка и частичного (исполнения) порядка.

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

переполнение стека

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

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

Конечно. Более того, они должны быть «чистыми», то есть без побочных эффектов - вы не можете изменить порядок двух println! . Вот почему мы также обычно рассматриваем незавершение как эффект, потому что тогда все сводится к «чистым выражениям можно переупорядочить» и «незавершенным выражениям нечисто» (impure = имеет побочный эффект).

Разделение также потенциально нечисто, но только при делении на 0 - что вызывает панику, т. Е. Эффект контроля. Это не наблюдается напрямую, но косвенно (например, если обработчик паники печатает что-то в stdout, что затем становится наблюдаемым). Таким образом, деление можно переупорядочить, только если мы уверены, что делим не на 0.

У меня есть демонстрационный код, который, я думаю, может быть этой проблемой, но я не совсем уверен. При необходимости я могу поместить это в новый отчет об ошибке.
Я поместил его код в репозиторий git по адресу https://github.com/uglyoldbob/rust_demo

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

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

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

@uglyoldbob Что на самом деле происходит в вашем примере, было бы более ясно, если бы llvm-objdump не было впечатляюще бесполезным (и неточным). Этот bl #4 (который на самом деле не является допустимым синтаксисом сборки) здесь означает переход на 4 байта после конца инструкции bl , также известной как конец функции main , также известной как начало следующей функции. Следующая функция вызывается (когда я ее создаю) _ZN11broken_loop18__cortex_m_rt_main17hbe300c9f0053d54dE , и это ваша фактическая функция main . Функция с незапутанным именем main - это не ваша функция, а совершенно другая функция, созданная макросом #[entry] предоставленным cortex-m-rt . Ваш код фактически не оптимизируется. (Фактически, оптимизатор даже не работает, поскольку вы строите в режиме отладки.)

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