Hacker News new | ask | show | jobs
by nkurz 3707 days ago
I've been using a lot of inline assembly lately, and while the Stockholm syndrome might be in effect, I'm coming to like the GCC syntax. For me, main thing that has helped has been to adopt a consistent syntax. Here's some examples of what I'm currently using for an AVX2 popcnt optimization, with some explanation.

  #define ASM_VEC_BYTE_COUNT_SET(vec, sum, mask, shuf)                  \
    __asm volatile ("vpsrld $4, %[VEC], %[SUM]\n"                       \
                    "vpand %[MASK], %[VEC], %[VEC]\n"                   \
                    "vpand %[MASK], %[SUM], %[SUM]\n"                   \
                    "vpshufb %[VEC], %[SHUF], %[VEC]\n"                 \
                    "vpshufb %[SUM], %[SHUF], %[SUM]\n"                 \
                    "vpaddb %[VEC], %[SUM], %[SUM]\n" :                 \
                    /* rd/wr ymm */ [VEC] "+&x" (vec),                  \
                    /* write ymm */ [SUM] "=&x" (sum) :                  \
        	    /* read ymm  */ [MASK] "x" (mask),                  \
                    /* read ymm  */ [SHUF] "x" (shuf))
1) Try to use the %[symbolic] syntax rather than %[n] numeric. It's slightly longer to write, but usually clearer to read. Use upper case for the symbolic name. Put your inputs one per line, with a preceding comment.

2) If you are using the same assembly more than once in your program, declare your assembly within a #define macro, then use the macro in your code.

3) Use "__asm volatile". Declaring "volatile" is not required, but once you are writing inline assembly you usually know more than the compiler about where the block should go.

5) If you have multiple lines of assembly and output registers, you are almost always safer to use "+&" and "=&" for your constraint rather than just "+" or "=". Search for "early clobber" for details.

6) Strongly prefer single type constraints. The more flexibility you give the compiler, the more likely it will defeat your efforts at optimization. Use explicit memory addressing modes rather than "m". The modifier "c" is needed for the offset.

  #define ASM_VEC_LOAD_OFFSET_MEM(off, mem, vec)                    \
    __asm volatile ("vmovdqu %c[OFF](%[MEM]), %[VEC]\n" :           \
                    /* destination */ [VEC] "=x" (vec) :            \
                    /* byte offset */ [OFF] "i" (off),              \
                    /* mem address */ [MEM] "r" (mem))
7) The register constraints for vectors are tricky, because the "x" constraint is used for both XMM and YMM vectors. There is no way to specify that one wants only one or the other. This sort of makes sense, since in hardware they share the same register. You can use the "q" modifier when you need to specify XMM syntax in the output when you need both forms of the same vector.
4 comments

3 - using volatile for asm that doesn't have otherwise inexpressible side effects has the same askance that using it for thread safety has. If you think you need it, maybe you needed to add a "memory" clobber instead.

5 - I can't think of any meaning early clobber has on an input+output constraint ("+")?

6 - there are many cases where you really do want to give the compiler flexibility in addressing modes. Unfortunately clang tends to ignore that and generate (reg) regardless.

7 - not really different than GPRs; you use "r" as the constraint then a modifier like "k" for the size.

I guess the lesson is that yeah gcc inline asm is powerful, but they try to leave it undocumented for a reason. Also, who stole number 4?

re 3: If it were for correctness, I'd agree. But I don't need volatile to make it work, I need it to produce the assembly I want. If one instruction can execute only on Port 1 (popcnt) and the other can execute on Ports 0, 1, 5, or 6, there's sometimes a 50% performance difference based on the order two seemingly independent instructions are executed. Volatile also prevents the compiler from hoisting loads ahead of my inline assembly, which sometimes makes a difference. Clobbering "mem" might force other reloads that I don't want to happen.

re 5: Barring compiler bugs, I think you'd be right if correctness was the only issue. But I'm pretty sure I've sometimes solved problems by adding it, although this may have been when working around the POPCNT bug that added a false dependency on the output. It also might have been when reading and writing a variable multiple times?

re 6: In theory, yes. But usually in these cases you should be writing intrinsics or straight C instead of inline assembly. The place where this comes up most for me is when I have two variables that use the same index, and I want to ensure "DEC/JNZ" fusion at the end of the loop. If I let the compiler choose, it will find a way to defeat me by incrementing both array addresses. The other case is when you explicitly want a store to use Port 7 for address generation, which only happens without an index register.

re 7: Yes, I just personally find it more confusing because "x" fits so well with "XMM", and thus it feels odd to use it when you want only a "YMM". Also, see here for problems with a Clang and %q[VEC]: http://stackoverflow.com/questions/34459803/in-gnu-c-inline-...

re 4: Oops, I forgot to renumber. I had another comment suggesting that one always use the "V" VEX prefix on vector commands and the explicit output register, but deleted it because it seemed off topic.

Cannot disagree more about #3. You almost never want asm volatile. The compiler is mostly doing data flow analysis, and I've seen so many programmers who don't understand that. So, if the compiler's data flow analysis doesn't put your asm block where you want, you just give up and put "volatile" on it. NO! Just let the compiler figure it out. You may be smarter about generating the assembly in this case, but the compiler is still very good at putting the assembly in the right place in your code. Usually, if I see "asm volatile" in someone's code, I step back and think "there's probably something wrong with the assembly" and I go back and read the manual on asm operand constraints, and then I find something wrong with the constraints. With the correct constraints / clobbers in place, my experience is that removing "volatile" only improves things.

Of course this is not true for synchronization primitives and the like.

I understand that most others share your position, and mostly agree when it comes to volatile variables. I'd also agree with you if removing "volatile" caused the code to break. But I think that it can be necessary for performance, and don't think that there are true downsides. I believe if you are using assembly it is because you don't want the compiler to attempt any further optimizations. For the cases when I want to drop to assembly, it's because I've already decided the register allocation and instruction ordering I want, and will verify the assembly that is generated.

My goal is to "lock in" an established level of performance once I've achieved it, so that compiler upgrades or changes don't result in performance drops. I often compare the output of multiple compilers with a matrix of optimization flags, choose the best blocks from each, and then hand-optimize from there while cross-referencing Agner's handbooks with Likwid's performance reports. If I've chosen to use inline assembly, the chances that the compiler will succeed in further optimizing my code is very low.

I realize it's not a popular view, but I think that using volatile with __asm is usually the correct approach. If you don't need "volatile", you probably should be using an intrinsic instead. I think the alternative (which may in fact be the better solution) is dropping to straight assembly for the entire function or distributing binary code.

Yes, that is really a better solution: to write the whole function in assembly. "volatile" is just a poor substitute for that.
Other than the "code smell", what do you see as the main dangers of using "__asm volatile" rather than just "__asm"? Assuming that there are cases where I do get significantly better performance from specifying the exact ordering of instructions, what can I do to minimize these dangers while keeping the better performance?
The first danger is that "asm volatile" is basically a hack to get the output you want from the compiler. But the compiler is a rather complicated piece of software, and there is no guarantee that future versions of the compiler will still give you the desired output. Perhaps it works correctly now, but if you change your optimization settings are you sure that something unexpected won't happen? Remember that "asm volatile" can still be moved around. From the GCC manual[1]:

> Do not expect a sequence of asm statements to remain perfectly consecutive after compilation, even when you are using the volatile qualifier. If certain instructions need to remain consecutive in the output, put them in a single multi-instruction asm statement.

The second danger is that "asm volatile" hides incorrect operand specification. If you examine the assembly, you might get the wrong assembly, and adding "volatile" might fix it. However, the incorrect operand specification might cause problems in other parts of the code. These are harder to diagnose. Stack Overflow is littered with questions by people who specify asm operands wrong, add "volatile" to fix the assembly, but other things are still broken. My general procedure is to work with asm blocks at -O2 or higher without using volatile, and make sure I'm getting the desired results that way (unless I'm writing some synchronization primitives).

Yet it is just so damn easy to write larger, multi-statement asm blocks. With larger blocks, the intent of the programmer is clear. It becomes obvious to both the reader and to the compiler that the assembly should be emitted as-is, rather than moved or reordered.

Finally, you can often get the results you want with the auto-vectorizer, restrict, and __builtin_assume_aligned. Whenever that is possible I'd prefer it.

[1]: https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html

I'd recommend to use intrinsics for SIMD vectorization, which is portable to platforms that don't support the GCC syntax (e.g. Windows with MSVC). You can use Intel's Intrinsics Guide (https://software.intel.com/sites/landingpage/IntrinsicsGuide...) to find the intrinsics that corresponds to the instructions you are using.
Yes, that's a great link, and I agree that if you can get the performance you want with Intrinsics they are usually a better choice. But if you need compiler-portable high performance, I find that it can be really hard to get good performance on GCC, ICC, and Clang simultaneously with intrinsics.

Another approach that's not quite there yet but is becoming more possible is to use https://www.cilkplus.org to annotate your C code to force automatic vectorization. It's native to ICC, built-in to GCC 5.0+, and available as an extension to Clang: https://news.ycombinator.com/item?id=11550250

I never liked them.

The asm {} blocks of PC compilers are so much developer friendly.

I rather use an external Assembler than GCC's inline syntax.