Почему сохранение и загрузка с 256-битного вектора AVX2 имеют разные результаты в режиме отладки и выпуска?

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

use std::arch::x86_64::*;

fn main() {
    let key = [1u64, 2, 3, 4];
    let avxreg = unsafe { _mm256_load_si256(key.as_ptr() as *const __m256i) };
    let mut back_key = [0u64; 4];
    unsafe { _mm256_storeu_si256(back_key.as_mut_ptr() as *mut __m256i, avxreg) };
    println!("back_key: {:?}", back_key);
}

детская площадка

В режиме отладки:

back_key: [1, 2, 3, 4]

В режиме выпуска:

back_key: [1, 2, 0, 0]

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

Какая странная ориентация на собственный процессор работает. В режиме выпуска + RUSTFLAGS="-C target-Cpu=native"

back_key: [1, 2, 3, 4]

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

use std::arch::x86_64::*;

#[repr(align(256))]
#[derive(Debug)]
struct Key([u64; 4]);

fn main() {
    let key = Key([1u64, 2, 3, 4]);
    let avxreg = unsafe { _mm256_load_si256(&key as *const _ as *const __m256i) };
    let mut back_key = Key([0u64; 4]);
    unsafe { _mm256_storeu_si256((&mut back_key) as *mut _ as *mut __m256i, avxreg) };
    println!("back_key: {:?}", back_key);
}
  1. Почему это происходит?
  2. Есть ли исправление для этого конкретного случая использования?
  3. Может ли это исправление быть обобщено для ввода пользователем (например: если я хотел взять байтовый срез в качестве пользовательского ввода и выполнить ту же процедуру)

Ответы

Ответ 1

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

#[target_feature(enable = "avx2")]

Или скомпилируйте всю программу с помощью

RUSTFLAGS="-C target-feature=+avx2" cargo run --release

Первый вариант лучше, потому что он гарантирует, что инструкции SIMD, используемые в функции, будут скомпилированы надлежащим образом, только на вызывающем устройстве, чтобы проверить, что их процессор имеет эти возможности перед вызовом is_x86_feature_detected!("avx2"). Все это задокументировано, но было бы замечательно, если бы компилятор мог предупредить: "Эй, эта функция использует инструкции AVX2, но не была аннотирована с #[target_feature(enable = "avx2")] и программа не была скомпилирована с включенным AVX2 глобально, поэтому вызов этой функции - неопределенное поведение ". Это избавило бы меня от головной боли!

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

use std::arch::x86_64::*;

fn main() {
    unsafe { run() }
}

#[target_feature(enable = "avx2")]
unsafe fn run() {
    let key = [1u64, 2, 3, 4];
    let avxreg = _mm256_load_si256(key.as_ptr() as *const __m256i);
    let mut back_key = [0u64; 4];
    _mm256_storeu_si256(back_key.as_mut_ptr() as *mut __m256i, avxreg);
    println!("back_key: {:?}", back_key);
}

Некоторые примечания:

  1. main не может быть небезопасным и, следовательно, не может быть аннотирован target_feature, поэтому необходимо извлечь в другую функцию
  2. Это все еще предполагает, что процессор x86_64 работающий с кодом, обладает возможностями avx, поэтому убедитесь, что вы проверили перед вызовом
  3. Не стоит задумываться о том, почему версия отладки дает правильные результаты, так как ее запуск под релизом на моем домашнем компьютере также дает правильные результаты (под некоторыми заклинаниями). Взгляд на сборку показывает, что LLVM оптимизирован так или иначе, но это не особенно проницательно.