I'm currently working as an author and contributor to SSIM.js and jest-image-snapshot on behalf of Not a Typical Agency. A lot of the work I'm performing results in the creation of new algorithms to compare images in Javascript. After a lot of research and testing with AssemblyScript and WebAssembly, I've found that I can often get a higher-performing and more portable solutions with pure JS than I do with either of these two technologies. However, that only happens after performing an extensive code review of the generated assembly and performing many experiments.
What I'd like to understand is if there's a way to get Node.JS/libV8 to automatically vectorize sections of code. As an example, I have a two-pass loop that calculates prefix sums for each pixel in an image horizontally then vertically. Skipping over the horizontal prefix sum (which can be challenging to vectorize for a real performance improvement with pure assembly), the vertical prefix sum should be super simple to optimize. Here's an example:
for (let h = 0; h + 1 < height; ++h) {
for (let w = 0; w < width; ++w) {
let above = pSumArray[h * width + w];
let current = pSumArray[(h + 1) * width + w];
pSumArray[(h + 1) * width + w] = above + current;
}
}
This takes all of the preexisting horizontal prefix sum calculations, and adds adjacent lines in the image going forward, one line at a time, all the way until it reaches the end.
The assembler output looks something like this:
0x11afd3a4adb1 1d1 c4a17b1044c10f vmovsd xmm0,[rcx+r8*8+0xf]
0x11afd3a4adb8 1d8 4c8bc2 REX.W movq r8,rdx
0x11afd3a4adbb 1db 4503c6 addl r8,r14
0x11afd3a4adbe 1de 0f80ce020000 jo 0x11afd3a4b092 <+0x4b2>
0x11afd3a4adc4 1e4 443bc7 cmpl r8,rdi
0x11afd3a4adc7 1e7 0f83d1020000 jnc 0x11afd3a4b09e <+0x4be>
0x11afd3a4adcd 1ed c4a17b104cc10f vmovsd xmm1,[rcx+r8*8+0xf]
0x11afd3a4add4 1f4 c5fb58c1 vaddsd xmm0,xmm0,xmm1
0x11afd3a4add8 1f8 c4a17b1144c10f vmovsd [rcx+r8*8+0xf],xmm0
If you look closely, you can see it's performing all of the load, store, and add operations with 'sd' (single double) operations. This means that it's only working with one double at a time. The documentation on this can be found here.
This is relevant in this application since it's a relative certainty that width is a multiple of 2 if not 16. As a side effect, we could be performing twice as many adds at a time if we're on a machine that only supports SSE2 instructions, and up to 8 adds at a time if the machine supports AVX512. Even though node.js and libv8 seem to do a decent job of runtime detecting CPUs, I still haven't found a way to get it to automatically vectorize a loop like this. I've tried a handful of strategies that include specific conditionals (e.g. is width divisible by 8) and combining them with delooping (e.g. array[0] =above0+current0 array[1]=above1+current1 array[2]=above2+current2...
) but none have yielded any successful results.
Any help someone can provide me on this topic would be greatly appreciated.
Thanks so much,
Dan
jo
after theaddl
). Is that the outer-loop logic? And you're not showing the actual inner loop that is just contiguous array += array? The outer loop is clearly pretty far from doing enough loop-invariant analysis to even hoist the overflow check. But maybe the inner loop asm is simpler. – Carnyvmovsd
/ vaddsd / vmovsd sequence is actually inside a pretty large loop body from 1b0 to 210, with a lot of branching and integer moves. That's way worse than you'd expect for just looping over contiguous doubles incrementing r8 by 1. The add/jo and cmp/jnc you showed are both just overflow checks, not part of the useful part of the loop structure. No wonder it was confusing. But yeah, no wonder V8 is nowhere near vectorizing this. Unless this is not final optimized JIT output, but just a first pass? IIRC V8 TurboFan waits for profile feedback to find hot code to fully optimize. – Carnyadd r8, 1
which makes sense from thew++
. If it's not the right code, link the line number or something if there's an actual optimized loop that's more interesting. – Carnyunchecked
as:for (let h = 0, stride = 0; h < height - 1; ++h) { for (let w = 0; w < width; ++w) { unchecked(pSumArray[stride + width + w] += pSumArray[stride + w]); } stride += width; }
– Isomeric