Log in

View Full Version : about SIMD assembly


feisty2
22nd September 2016, 13:30
you just gotta code some SIMD assembly stuff manually like, compilers are not that intelligent at auto-vectorizing

and I got my hands on some basic x64 assembly like recently, now I could turn a simple C/C++ function into assembly like,


auto f(uint64_t x, uint64_t y) {
auto a = x;
a += y;
auto b = a * 2;
return b;
}


mindlessly translated to assembly(like what compilers do, probably)

f PROC
push rbp
mov rbp, rsp
sub rsp, 16
mov [rbp], rcx
add [rbp], rdx
mov r8, [rbp]
add r8, r8
mov [rbp-8], r8
mov rax, [rbp-8]
add rsp, 16
pop rbp
ret
f ENDP


or a bit optimized

f PROC
add rcx, rdx
add rcx, rcx
mov rax, rcx
ret
f ENDP


my question here is: should I go further on this, "standard" kind of assembly or maybe I'm good enough to turn to SIMD assembly already, and I could just stop my x64 assembly journey now?

and I also want some recommendations of SIMD(especially avx2) tutorials for newbies, I've been googling but the tutorials I found were too professional to me

shekh
22nd September 2016, 13:55
IMO writing full function in asm like this is damn too difficult. In many cases if you can express something with intrinsics (available for all simd) the generated code is ok (not worse than hard asm).

lvqcl
22nd September 2016, 14:41
mindlessly translated to assembly(like what compilers do, probably)

Actually, modern compilers are quite smart.

GCC 6.1 (-O2):

add rcx, rdx
lea rax, [rcx+rcx]
ret

MSVS 2015 U3 (Release):

lea rax, [rcx+rdx]
add rax, rax
ret

feisty2
22nd September 2016, 14:57
Actually, modern compilers are quite smart.

GCC 6.1 (-O2):

add rcx, rdx
lea rax, [rcx+rcx]
ret

MSVS 2015 U3 (Release):

lea rax, [rcx+rdx]
add rax, rax
ret


Yeah, auto-vectorizing is another story

feisty2
22nd September 2016, 15:00
IMO writing full function in asm like this is damn too difficult. In many cases if you can express something with intrinsics (available for all simd) the generated code is ok (not worse than hard asm).

Ok then....
Any recommendation of "immintrin.h" tutorials for newbies?

shekh
22nd September 2016, 15:09
I learn it like this: read intel`s instruction set reference manual, think how would I use every operation. Then try to find "how to do X" on stackoverflow

Parabola
22nd September 2016, 21:01
There is a third way, besides intrinsics or using a conventional assembler: JIT assembly. A JIT assembler let's you write assembly instructions in C++ and when the program initialises it assembles into data memory. Example in this project: https://github.com/kupix/havoc

JIT means no special toolchain is needed: you use standard C++ loops, functions and templates used instead of esoteric assembler macros. And you can get full, hand coded performance - or better if you can integrate run-time parameters into the byte code.

shekh
22nd September 2016, 22:14
This is interesting. Can you clarify few things:
Can I debug Xbyak-generated code? With line numbers and symbols.. does it have any symbols?
Does it compile anything? Looks like it needs to specify all instructions exactly, so I cannot use c++ syntax for loops, variables, functions etc?

Motenai Yoda
22nd September 2016, 23:18
why don't you use shift to left?

f PROC
add rcx, rdx
mov rax, rcx
shl rax
ret
f ENDP

Myrsloik
23rd September 2016, 14:40
you just gotta code some SIMD assembly stuff manually like, compilers are not that intelligent at auto-vectorizing
...

my question here is: should I go further on this, "standard" kind of assembly or maybe I'm good enough to turn to SIMD assembly already, and I could just stop my x64 assembly journey now?

and I also want some recommendations of SIMD(especially avx2) tutorials for newbies, I've been googling but the tutorials I found were too professional to me

Don't start with AVX2. Use all the SSE versions but AVX actually isn't that good for integer things (because intel got lazy and most instructions act like ymm is just two xmm registers with the same operation applied to them, this actually makes writing avx2 versions of functions more annoying). And compatibility, that's another thing.

Scalar asm is a waste of time. Unless some aliasing rule makes the compiler reload a value far too many times it'll always be just as fast as you are, possibly faster. If it's not it just means you forgot to sprinkle the restrict keyword in the right places.

Your example is also for too simple to use as a discussion point, there is no SIMD optimization possible unless you tell me you've got a long vector of int64s you want to add and multiply.

I'd also recommend that you either use intrinsics or yasm/nasm+x86inc.asm to write things that work everywhere. Don't listen to people who tell you to use a JIT. Most functions don't need it. The only place in all of VapourSynth that benefits from it is the Expr filter since it greatly cuts down the number of jumps needed in the code.

Post a longer example and I'll demonstrate the dark art of SIMD...

shekh
23rd September 2016, 16:33
How do you compute b = a*65535/1023?

this can be coded as b = (a*c0) >> 16 with 32bit multiply
we have PMULLD unfortunately sse4_1
or PMADDWD sse2, but not very suitable
or MULPS with conversions to/from floating point
or compute as b = a*64 + (a*(c0-64)) >> 16 with 16bit multiply

I suspect the last method is better

Myrsloik
23rd September 2016, 16:46
How do you compute b = a*65535/1023?

this can be coded as b = (a*c0) >> 16 with 32bit multiply
we have PMULLD unfortunately sse4_1
or PMADDWD sse2, but not very suitable
or MULPS with conversions to/from floating point
or compute as b = a*64 + (a*(c0-64)) >> 16 with 16bit multiply

I suspect the last method is better

You have to say the range of both the input (a) and output you need. And if it's signed or unsigned. both of those make a big difference once you want to find suitable instructions...

shekh
23rd September 2016, 17:04
You have to say the range of both the input (a) and output you need. And if it's signed or unsigned. both of those make a big difference once you want to find suitable instructions...

Both input and output is uint16. Input in range 0-1023 and output in range 0-65535.
ps. This is like changing bitdepth of image plane (lots of values, same scaling constant).

Myrsloik
23rd September 2016, 17:47
Both input and output is uint16. Input in range 0-1023 and output in range 0-65535.
ps. This is like changing bitdepth of image plane (lots of values, same scaling constant).

I'd just do (i << 6) | (i >> 4) and call it done. Nice and easy and only differs by 1 at most.

Oh, and b = (a*65535 + 512)/1023 probably gives better rounding

shekh
23rd September 2016, 18:06
I'd just do (i << 6) | (i >> 4) and call it done. Nice and easy and only differs by 1 at most.

Awesome. Did you run it through all values to find difference by 1?

Oh, and b = (a*65535 + 512)/1023 probably gives better rounding

I left rounding as exercise for implementation. Normally not hard to add (but your method does not need rounding at all, at least for few values I tried).

Myrsloik
23rd September 2016, 18:07
Awesome. Did you run it through all values to find difference by 1?



I left rounding as exercise for implementation. Normally not hard to add (but your method does not need rounding at all, at least for few values I tried).

I did. (i << 6) | (i >> 4) vs (a*65535 + 512)/1023 only has 234 values that are different.