Comment by burntsushi
8 years ago
No, it's not about panicking. It's undefined behavior to run code compiled with CPU features that aren't supported by the current CPU. See: https://github.com/rust-lang/rfcs/blob/master/text/2045-targ...
There are some other ideas for making it easier to reason about safety at this level: https://github.com/rust-lang/rfcs/pull/2212
Can you point to where you heard about unsupported SIMD causing a panic? I'd like to fix that because it's really wrong!
I mean that it really shouldn’t be UB to call a function compiled for an unsupported target feature. Following that link, I see two arguments that it’s UB:
1. A multibyte NOP might be used. Supposedly there might be a multibyte NOP that older CPUs will decode as a jump. I am not sure I believe this. Is there an example?
2. int3 might happen, causing SIGTRAP. I see no explanation of how this would occur.
So I think that, if LLVM really has UB if the wrong target is used, it should be fixed. Arguable the old Knights Landing instructions are an exception, but those are basically dead. Maybe non-x86 targets are different.
Also, I have a suggestion for a potentially much nicer way to deal with safety: use the type system instead of magic annotations. Have a function like GetAVX2() -> Option<AVX2>. Teach Rust that code that statically has a live AVX2 object (as a parameter, say) can use AVX2. Other than the code generation, this could be done in stable rust right now.
_This_ is why I linked to my undefined behavior post. Your comment is about as clear an example of being in the semi-portable camp as any I've seen. And I'm not blaming you, because in C you _can't_ do SIMD in the standard camp, so it's actually one of the more compelling reasons to remain in semi-portable. Rust is different though.
Also: the linked crate does use the type system in pretty much this way so that code that clients can be safe. However, there are limitations; it's not just whether a particular instruction can be used, which remains immutable once it's detected, but also which _registers_ (and, by extension, calling convention) can be used. That varies from function to function, and requires the `#[target_feature(enable)]` annotation to control, so just having an `Avx` type in hand is not quite enough to ensure that you're in a context where using the ymm registers is ok, and the intrinsic will be inlined to a "V" variant asm instruction. This is discussed in some detail in the "caveats" section.
I'm not experienced enough in compilers at this level to make a prescriptive argument here. My comment was just intended to be descriptive. I think it would be well worth the effort to dig into the LLVM side of things here to root out the specific reasons for UB. Intuitively though, it makes sense to me that it would be UB. I'd be surprised if it weren't. It seems like it should be reasonable for compilers to assume that the execution of an instruction implies that instruction is supported on the current CPU.
Your type system idea works great for simple cases. It was the very first thing I did in my own SIMD code.
There is the case of LZCNT and BSR. On processors that do not support the former, LZCNT is interpreted as (REP) BSR, but the instructions have different behavior that could result in silent failures.