5 ms·
Naive version to explore autovectorization: https://gcc.godbolt.org/z/bcefvznG3 https://gcc.godbolt.org/z/bcefvznG3 Yes, there's some amount of autovectorizat
by dietrichepp 4y ago
Naive version to explore autovectorization:
https://gcc.godbolt.org/z/bcefvznG3 https://gcc.godbolt.org/z/bcefvznG3
Yes, there's some amount of autovectorization here. Seems like a mess of a function. Here's something more like tolower8():
https://gcc.godbolt.org/z/h8GTanodd https://gcc.godbolt.org/z/h8GTanodd
The generated code definitely looks funny to me. Here is a manual vectorization, which is shorter:
https://gcc.godbolt.org/z/1e4odEsKq https://gcc.godbolt.org/z/1e4odEsKq
The issue of loading into your vector register... well, there are some dirty tricks for that. The 16-byte slice containing the first byte, and the 16-byte slice containing the last byte, can both be loaded into registers and then shifted around in order to construct the desired value. Note the careful wording here... these slices might be the same slice. Or you can iterate over the 16-byte slices containing the array, and shift as you go, if you're storing into a different location. Or you can use various masked load/store operations on various architectures.
- dragontamer 4y ago> The issue of loading into your vector register... well, there are some dirty tricks for that. Good discussion. I think the only method you haven't talked about is the simple "unaligned load" instructions (which might be the simplest, and most portable way, to do this). I know that ARM and x86 both can do unaligned loads no problem, but at a possible performance penalty.
- dietrichepp 4y agoThe reason I didn’t mention unaligned load is because it can cross a page boundary and fault. With aligned loads, you can avoid this. (Edit: The idea is that with an aligned load, you only load 16-byte blocks that contain valid data. This can’t page fault, but you can load data you didn’t want, which is ok. Writing is trickier because the write may have to be atomic and can’t spill.)
- zen_1 4y agoAnother (admittedly quite niche) gotcha I've experienced with aarch64 unaligned SIMD loads specifically is that they will fail (raising a data abort exception) if they access memory that isn't specifically designated as "normal ram" by the MMU. This can happen if you're writing code for an embedded system, or even if you're calling memcpy with a too-smart-for-it's-own-good libc (such as Newlib, that ships with the aarch64-none-elf toolchain ARM provides...) implementation without first setting the MMU up.
- nimish 4y ago> Seems like a mess of a function It has to deal with an arbitrary n, so it's going to have some messy bits to deal with the "fringe": https://www.sigarch.org/simd-instructions-considered-harmful/ https://www.sigarch.org/simd-instructions-considered-harmful...
- dietrichepp 4y agoIt’s more of a comment on how messy that code gets at -O3… it looks a lot messer than the -O2 code. (I think, it’s time, to say that “‘X considered harmful’ considered harmful”.)
- dzaima 4y ago-O2 doesn't have any usage of SIMD/vectorization though. Of course it'd be simpler.
- dietrichepp 4y ago...Have you looked at the -O3 assembly that I'm talking about? There's a short SIMD loop, which is simple, and fairly easy to understand. At -O2, you get a short scalar loop, which is simple, and fairly easy to understand. If you were writing this function by hand, you might combine the SIMD loop with the scalar loop. That's not what happens, though. Instead, you get two versions of the SIMD loop which handle different alignments (unnecessary) and then, what appears to be an unrolled version of the scalar loop. What’s going on here is that there are a bunch of transformations enabled at -O3 that come along with autovectorization. You don’t just get autovectorization by itself, you also get transformation passes that are designed to put your code into a state where vectorization is easier. You also sometimes get a number of transformations which are (empirically) dubious, in the sense that the resulting code is both larger and slower.
- dzaima 4y agoThere's only one SIMD loop - a 128-bit one. There's a 64-bit SIMD segment for handling handling when the tail has ≥8 items (which is gonna be a pretty decent speedup for 8≤n≤15 byte inputs). If you're gonna operate on only very large inputs, it's quite unnecessary, yeah, but for a lowercasing function small inputs will be very common. The unrolling of the scalar tail is quite unnecessary though (gcc does so even with -fno-unroll-loops). clang doesn't[0], though it does unroll the main loop (though that can be disabled with -fno-unroll-loops). [0]: https://gcc.godbolt.org/z/n85aavnEo https://gcc.godbolt.org/z/n85aavnEo