Could someone show me the most straight forward equivalent in vanilla C? I assume there is no direct equivalent as temporary storage will be needed. But that's fine and would further serve the purpose of explaining why Rust is cool.
Thanks.
Could someone show me the most straight forward equivalent in vanilla C? I assume there is no direct equivalent as temporary storage will be needed. But that's fine and would further serve the purpose of explaining why Rust is cool.
Thanks.
For an imperative translation see https://www.reddit.com/r/programming/comments/5fpghn/zerocos...
Doing an exact translation to C is hard. The C that does the same thing, or C that demonstrates these abstractions that are boiling away?
uint32_t *buffer = ...;
uint64_t coefficients[12] = {...};
uint16_t qlp_shift = ...;
uint32_t *bufp = &buffer[...];
uint64_t sum;
for (size_t i = 0; i < 12; i++)
sum += coefficients[i] * bufp[i-12];
uint64_t prediction = sum >> qlp_shift;
*bufp += (uint32_t)prediction;
Edit: this is incorrectPun intended? If so, nicely done.
My point is not that the snippet is bad, just that Rust feels, looks, and probably is, quite complex. As opposed to, for example, Haskell, which also introduced a bunch of nice, new, ideas.
> Rust feels, looks, and probably is, quite complex. As opposed to, for example, Haskell
Do you really think that Haskell is less complex than Rust, or am I just reading your comment wrong?Really? I've never used rust but the code was pretty clear to me except for the >> shift part.
> But I think I would be able to understand the snippet in most imperative and functional languages.
In Python (ignoring the shift thingy) I'd write
prediction = sum(map(lambda c, s: c*s, zip(coefficients, buffer[i-12: i])))
To me, both are equally clearI know, I know, the C code you write never has buffer overruns, just like the C code I write never does, well, except that one time, but then I caught it in the debugger during testing and it was easy to fix. Oh, and then that other time, but I caught that one too, and then.....
Nah, I did that on purpose because I remember the time I accidentally swapped the 'value' and 'size' parameters of memset, trashing the stack, and so now I go the 'safer' option to just leave things uninitialised. Not to mention the performance increase I get from avoiding a memset that isn't necessary, so long as I'm really, really careful and never make a mistake, which I almost never do.
And I cannot tell if you are serious, trolling or sarcastic, because I know people who think this way, think this way is a joke and think this way is the bane of their professional lives.
No it's not. In this line:
.zip(&buffer[i - 12..i])
you can just put any indices and it will compile. for (size_t i = 0; i < 12; i++) {
sum = 0;
for (size_t j = 0; j < 12; j++)
sum += coefficients[j] * bufp[i + j];
buffer[i + 12] += sum >> qlp_shift;
} for (size_t i = 0; i < 12; i++) {
int64_t sum = 0;
int32_t *bufp = buffer + i;
for (size_t j = 0; j < 12; j++)
sum += coefficients[j] * bufp[j];
*bufp += (int32_t)(sum >> qlp_shift);
}
I think this will put it in a form where the compiler should vectorize it[1], probably making the loop about twice as fast as the version shown: 3 vector loads, 3 vector multiplications, 3 vector additions and a horizontal sum versus 16 scalar loads[2], 12 scalar multiplications and 12 scalar additions.[1] I presume the "various reasons the above snippet is not as obvious to vectorise as it might seem at first sight" is because the coefficients[] are 64-bit and buffer[] is 32-bit. But a smart compiler should be able to use PMOVSXDQ (packed move sign extend double-word to quad-word) here. Unless there is some other issue, I think the author may be underestimating the benefit of vectorizing here.
[2] While the author is right that most of coefficients stay in registers, there isn't actually room for all 12, so 4 of them are loaded from the stack every iteration. Since only 3 128-bit vector registers are required, they should remain there. I'm not sure if the excess loads are actually a bottleneck, though.
Memory safety without a hit in performance seems like a great thing, and this is a fine example that it's becoming possible. The point is not that C is always fine. In this case, I think the actual conclusion is something like: C and Rust perform equally here, both have about 50% of the theoretical assembly performance on modern processors, thus better vectorization would help both of them. But if all else is equal, choose the safe one.
I played around with you snippet using the compiler explorer https://godbolt.org/ . I tried a couple versions of clang, gcc, and icc and none of those were able to auto-vectorize it. Not sure why. Kind of a shame, since there would definitely be a speedup.
In addition, I discovered a couple other issues. I was embarrassed to see that I'd messed up the loop bounds in my example. But the bigger one is that AVX2 and earlier do not have a 64-bit x 64-bit -> 64-bit vector multiplication instruction. VPMULLQ was only added with AVX512, possibly because it requires a 512-bit intermediate.
This means that with 64-bit coefficients, the vector approach has to do the multiplications twice, and shift, and add. This is likely going to be slower than the scalar approach. But if you were able to switch to 32-bit (or floating point) coefficients, I think the vectorized solution looks pretty good. ICC estimates a 1.4x speedup, but I think that gets better with longer buffers.
I put the version with int32_t coefficients up here: https://godbolt.org/g/GXNVe2
For example: https://is.gd/hrUf7q