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

301

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

void adds(int  *a, int *b) {
    *a += *b;
    *a += *b;
}

при компиляции clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)с -O3флагом он выдает

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)  # The first time
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)  # The second time
   a:    c3                       retq

Здесь код сохраняется в (%rdi)два раза в случае int *aи int *bпсевдоним.

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

void adds(int * restrict a, int * restrict b) {
    *a += *b;
    *a += *b;
}

Тогда Clang выпустит более оптимизированную версию двоичного кода:

0000000000000000 <adds>:
   0:    8b 06                    mov    (%rsi),%eax
   2:    01 c0                    add    %eax,%eax
   4:    01 07                    add    %eax,(%rdi)
   6:    c3                       retq

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

Когда я тестирую с помощью приведенного ниже кода и компилирую его с rustc 1.35.0помощью -C opt-level=3 --emit obj,

#![crate_type = "staticlib"]
#[no_mangle]
fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

он генерирует:

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)
   a:    c3                       retq

Это не воспользоваться гарантией, что aи bне может псевдоним.

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

Это потому , что есть еще шанс , что aи bможет псевдоним, даже в безопасном Rust?

Zhiyao
источник
3
godbolt.org/z/aEDINX , странно
Stargateur
76
Дополнительное замечание: « Поскольку Rust гарантирует (за исключением небезопасного кода), что две изменяемые ссылки не могут быть псевдонимами », стоит отметить, что даже в unsafeкоде псевдонимы изменяемых ссылок не допускаются и приводят к неопределенному поведению. Вы можете использовать псевдонимы-указатели, но unsafeкод на самом деле не позволяет игнорировать стандартные правила Rust. Это просто распространенное заблуждение, на которое стоит обратить внимание.
Лукас Калбертодт
6
Мне потребовалось некоторое время, чтобы выяснить, к чему приводит пример, потому что я не разбираюсь в чтении asm, поэтому в случае, если это помогает кому-то еще: все сводится к тому, могут ли две +=операции в теле addsмогут быть интерпретированы как *a = *a + *b + *b. Если указатели не псевдоним, они могут, вы даже можете увидеть , что составляет b* + *bво втором ассемблерного листинга: 2: 01 c0 add %eax,%eax. Но если они делают псевдоним, они не могут, потому что к тому времени, когда вы добавляете *bвторой раз, он будет содержать значение, отличное от значения первого раза (того, которое вы храните в строке 4:первого списка asm).
dlukes

Ответы:

364

Ржавчина первоначально сделал включить LLVM в noaliasатрибут, но этот причиненный miscompiled код . Когда все поддерживаемые версии LLVM больше не будут неправильно компилировать код, он будет снова включен .

Если вы добавите -Zmutable-noalias=yesв параметры компилятора, вы получите ожидаемую сборку:

adds:
        mov     eax, dword ptr [rsi]
        add     eax, eax
        add     dword ptr [rdi], eax
        ret

Проще говоря, Rust повсеместно использует эквивалент restrictключевого слова C , гораздо более распространенный, чем любая обычная программа на C. Это использовало угловые случаи LLVM больше, чем он мог правильно обработать. Оказывается, программисты на C и C ++ просто не используют так часто, как в Rust.restrict&mut

Это происходило несколько раз .

  • Руст с 1.0 по 1.7 - noaliasвключен
  • Руст от 1,8 до 1,27 - noaliasотключено
  • Руст от 1.28 до 1.29 - noaliasвключен
  • Руст 1.30 через ??? - noaliasотключен

Связанные проблемы ржавчины

Shepmaster
источник
12
Это не удивительно. Несмотря на свои широко распространенные требования мультиязычности, LLVM был специально разработан как бэкэнд C ++ и всегда имел сильную тенденцию задыхаться от вещей, которые не выглядят достаточно похожими на C ++.
Мейсон Уилер
47
@MasonWheeler Если вы перейдете к некоторым проблемам, вы можете найти примеры кода на C, которые используют restrictи неправильно компилируют как в Clang, так и в GCC. Это не ограничивается языками, для которых недостаточно «C ++», если только вы не считаете C ++ сам в этой группе .
овчарка
6
@MasonWheeler: Я не думаю, что LLVM действительно был разработан вокруг правил C или C ++, а скорее вокруг правил LLVM. Он делает предположения, которые обычно верны для кода C или C ++, но из того, что я могу сказать, дизайн основан на модели статических данных-зависимостей, которая не может обрабатывать сложные угловые случаи. Это было бы хорошо, если бы он пессимистически предполагал зависимости данных, которые не могут быть опровергнуты, но вместо этого он обрабатывает как действия no-ops, которые будут записывать хранилище с таким же битовым шаблоном, как и он, и которые имеют потенциальные, но не доказуемые зависимости данных от читай и пиши.
суперкат
8
@supercat Я прочитал ваши комментарии несколько раз, но признаюсь, что озадачен - я понятия не имею, что они имеют отношение к этому вопросу или ответу. Неопределенное поведение здесь не вступает в игру, это «просто» случай нескольких проходов оптимизации, плохо взаимодействующих друг с другом.
овчарка
2
@avl_sweden, чтобы повторить, это просто ошибка . Этап оптимизации развертывания цикла (не?) Не полностью учитывал noaliasуказатели при выполнении. Он создавал новые указатели на основе входных указателей, неправильно копируя noaliasатрибут, хотя новые указатели делали псевдоним.
Шепмастер