Skip to content

vreinterpret on big endian swaps byte order #1904

Closed

Description

@usamoi

Consider the following code:

#![allow(improper_ctypes_definitions)]

use core::arch::aarch64::*;

#[inline(never)]
#[unsafe(no_mangle)]
#[target_feature(enable = "neon")]
extern "C" fn f(x: uint8x16_t) -> uint16x8_t {
    vreinterpretq_u16_u8(x)
}

fn main() {
    unsafe {
        let src = [0_u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
        let x: uint8x16_t = core::mem::transmute(src);
        let y: [u8; 16] = std::mem::transmute(f(x));
        dbg!(y);
    }
}

cargo +nightly run --target aarch64_be-unknown-linux-gnu -Zbuild-std --release:

[src/main.rs:17:9] y = [
    1,
    0,
    3,
    2,
    5,
    4,
    7,
    6,
    9,
    8,
    11,
    10,
    13,
    12,
    15,
    14,
]

llvm-objdump -d:

0000000000028870 <f>:
   28870: 4e200800     	rev64	v0.16b, v0.16b
   28874: 6e004000     	ext	v0.16b, v0.16b, v0.16b, #0x8
   28878: 4e201800     	rev16	v0.16b, v0.16b
   2887c: 4e200800     	rev64	v0.16b, v0.16b
   28880: 6e004000     	ext	v0.16b, v0.16b, v0.16b, #0x8
   28884: d65f03c0     	ret

vreinterpret should not generate instructions, but it does, and it also swaps the byte order of each element.

Clang does not generate any instructions with --target=aarch64_be-linux-gnu: https://godbolt.org/z/Erse7GnEK

Activity

  1. sayantn commented on Sep 6, 2025

    @sayantn
    Contributor

    This seems to be an oversight in the implementation (this also means the tests are probably doing something wrong too, they compare output with clang, it shouldn't have been passing for aarch64_be)

  2. adamgemmell commented on Sep 15, 2025

    @adamgemmell
    Contributor

    These intrinsics were doing some unnecessary swizzling but we're not sure if removing this has changed their behaviour. Our tests pass before and after the above fix and they appear to be working correctly.

    Can I confirm that you're running on a BE platform and that the above fix works for you @usamoi ? (it hasn't made it into nightly rust yet, happy to wait until it does)

  3. usamoi commented on Sep 15, 2025

    @usamoi
    ContributorAuthor

    Can I confirm that you're running on a BE platform and that the above fix works for you

    I don't have a aarch64_be machine, but the output becomes correct on qemu now.

    [src/main.rs:17:9] y = [
        0,
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8,
        9,
        10,
        11,
        12,
        13,
        14,
        15,
    ]
    

    And, objdump shows it's compiled to a nop now.

    0000000000028af0 <f>:
       28af0: d65f03c0     	ret
    
  4. sayantn commented on Sep 22, 2025

    @sayantn
    Contributor

    #1924 reverts the PR because it made the testsuites fail, see #1923. We would probably need to discuss where it is going wrong

  5. sayantn commented on Oct 5, 2025

    @sayantn
    Contributor

    @rustbot ping arm

    pinging people with more ARM expertise

  6. rustbot commented on Oct 5, 2025

    @rustbot
    Collaborator

    Hey Arm-interested people! This issue could use some guidance on how it can be
    resolved on Arm platforms.
    Could one of you weigh in please? In case it's useful, here are some
    instructions for tackling these sorts of bugs.
    Thanks!

    cc @adamgemmell @davidtwco @hug-dev @jacobbramley @JamieCunliffe @joaopaulocarreiro @raw-bin @Stammark

  7. Jamesbarford commented on Nov 19, 2025

    @Jamesbarford
    Contributor

    Helpful information from the docs

    Note1: LLVM Documentation (https://llvm.org/docs/BigEndianNEON.html#bitconverts);

    If a vector is loaded and then bitconverted to a different vector type before being stored, the round trip will currently be broken.

    [... follow the image on the right]

    This would produce a code sequence such as that in the figure on the right. The mismatched LD1 and ST1 cause the stored data to differ from the loaded data.

    Note2: Transmute:

    copies the bits from the source value into the destination value"

    docs

    Note3: On lanes:

    element 0 uses the least significant bits of the register

    thus lane 0 is always the least significant bits of the register regardless of endianness. docs

    Note4: vreinterpretq_u16_u8 Loads into Vd.16B and stores in Vd.8H changing the arrangement specifier from 16B (16bytes) to 8H (8 16bit halfwords). Also see Arms documentation here for some more examples and more indepth detail.

    Tracing the code

    Note: byte_view is the bytes produced with a hex dump NOT the numbers printed as hexidecimal.

    To make things slightly easier to follow in the annotated code below I have expanded;

    let y: [u8; 16] = std::mem::transmute(f(x));

    To

    let z = f(x);
    let y: [u8; 16] = std::mem::transmute(z);
    //start_bytes = 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    
    // no byte swapping - just defining the array
    let src = [0_u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
    
    // no byte swapping - This is because the resultant type is essentially the same as above
    // byte_view = 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    let x: uint8x16_t = core::mem::transmute(src);
    
    // ld1 { v0.16b } -> st1 { v0.8h }
    // We are interpreting each pair of bytes as a big-endian halfwords;
    // [00 01] [02 03] [04 05] [06 07] [08 09] [0a 0b] [0c 0d] [0e 0f]
    // numerical_view = [256, 770, 1284, 1798, 2312, 2826, 3340, 3854]
    // which becomes in memory (because of big-endian element reordering rules)
    // byte_view      = 01 00 03 02 05 04 07 06 09 08 0b 0a 0d 0c 0f 0e
    let z = f(x); // vreinterpretq_u16_u8(x);
    
    // no byte swapping - Note that this matches to LLVMs documentation of
    // "the round trip will currently be broken"
    // byte_view = 01 00 03 02 05 04 07 06 09 08 0b 0a 0d 0c 0f 0e
    // numerical_view = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10, 13, 12, 15, 14]
    let y: [u8; 16] = std::mem::transmute(z);

    If we take x[0] = 0u8 and x[1] = 1u8, then in big-endian mode the register holds the bits as ...0x0100. Interpreting those bits in place as a u16 via vreinterpret yields the value 256. By contrast, transmute operates on the memory layout, where the bytes appear as 0x00 01, producing the value 1. This explains why what you are seeing, while surprising, is in fact correct behaviour according to the ARM and LLVM big-endian NEON models, even though it looks counter-intuitive at first glance. To get what you expect back you'd want to use another of the vreinterpret intrinsics as opposed to a transmute.

  8. usamoi commented on Nov 19, 2025

    @usamoi
    ContributorAuthor

    @Jamesbarford Thank you for the explanation! Since it's expected, I'm closing this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions