View Full Version : speed of reinterpret_cast trick goes south
feisty2
16th June 2016, 16:52
#include <iostream>
#include <cstdlib>
#include <cstdint>
#include <ctime>
auto main()->void {
uint8_t a[] = { 1,2,3,4,5,6,7,8 };
uint8_t b[] = { 42,42,42,42,42,42,42,42 };
auto &fake_a = *reinterpret_cast<uint64_t *>(a);
auto &fake_b = *reinterpret_cast<uint64_t *>(b);
auto t0 = clock();
for (auto i = 0ull; i < 10000000ull; ++i);
auto t1 = clock();
for (auto i = 0ull; i < 10000000ull; ++i) {
fake_a += fake_b;
fake_a -= fake_b;
}
auto t2 = clock();
for (auto i = 0ull; i < 10000000ull; ++i) {
a[0] += b[0];
a[1] += b[1];
a[2] += b[2];
a[3] += b[3];
a[4] += b[4];
a[5] += b[5];
a[6] += b[6];
a[7] += b[7];
a[0] -= b[0];
a[1] -= b[1];
a[2] -= b[2];
a[3] -= b[3];
a[4] -= b[4];
a[5] -= b[5];
a[6] -= b[6];
a[7] -= b[7];
}
auto t3 = clock();
std::cout << (t2 - t1 - (t1 - t0)) << std::endl << (t3 - t2 - (t1 - t0)) << std::endl;
system("pause");
}
http://i.imgur.com/1btOpkD.png
all compiler optimizations disabled
obviously, t3 - t2 - (t1 - t0) should equal to 8 * (t2 - t1 - (t1 - t0)) like theoretically, and the reality tells a different side of the story..
how and why is this happening? :confused:
raffriff42
17th June 2016, 04:46
Question - why should t2 - t1 (reinterpret_cast time) equal t3 - t2 (bytewise time)? The bytewise loop has more statements; therefore it should be slower, shouldn't it?
Anyway I get a less severe difference in times than you do (VC2010 express):C:\Projects\doomtest>doomtest
time, reinterpret_cast: 62
time, bytewise: 78
Press any key to continue . . .
C:\Projects\doomtest>doomtest
time, reinterpret_cast: 47
time, bytewise: 94
Press any key to continue . . .
C:\Projects\doomtest>doomtest
time, reinterpret_cast: 47
time, bytewise: 63
feisty2
17th June 2016, 05:06
Question - why should t2 - t1 (reinterpret_cast time) equal t3 - t2 (bytewise time)? The bytewise loop has more statements; therefore it should be slower, shouldn't it?
Anyway I get a less severe difference in times than you do (VC2010 express):C:\Projects\doomtest>doomtest
time, reinterpret_cast: 62
time, bytewise: 78
Press any key to continue . . .
C:\Projects\doomtest>doomtest
time, reinterpret_cast: 47
time, bytewise: 94
Press any key to continue . . .
C:\Projects\doomtest>doomtest
time, reinterpret_cast: 47
time, bytewise: 63
Well, I was saying =8* not =
maybe you turned compiler optimizations on or you were doing it under IA32 instead of EMT64 so the results are fairly different?
raffriff42
17th June 2016, 05:10
8*, right :o
And yeah, IA32
re-EDIT to return what I said that feisty2 replies to below:
"(BTW with optimizations on, t3 - t2 = 0!)"
...and after subtracting the empty loop (overhead test) sometimes the times were negative!
feisty2
17th June 2016, 05:21
That makes sense since + for uint64_t could be done with one simple instruction under EMT64 and much more complicated under IA32
with optimizations on, the compiler would be aware of a+=b a-=b basically does nothing, and will ignore it, and the runtime will be 0
colours
17th June 2016, 15:12
`reinterpret_cast` does not allow for dereferencing except under very specific conditions which are not satisfied in your code, so you're hitting undefined behaviour. It doesn't matter how fast it runs if it's wrong.
PS: Dereferencing a pointer cast from `uint8_t*` to `uint64_t*` is UB, but the reverse isn't, so do that instead.
feisty2
17th June 2016, 15:32
`reinterpret_cast` does not allow for dereferencing except under very specific conditions which are not satisfied in your code, so you're hitting undefined behaviour. It doesn't matter how fast it runs if it's wrong.
PS: Dereferencing a pointer cast from `uint8_t*` to `uint64_t*` is UB, but the reverse isn't, so do that instead.
But I think this could be some kind of SIMD, I've tested it and the results are correct, basically you are doing addition to 8 uint8_ts simultaneously as one uint64_t, why is that undefined behavior?
(I don't know shit about asm, c/c++ are the lowest level language I can take, I know avx could do this but I don't know how to do that...)
jackoneill
17th June 2016, 16:02
Apparently modern CPUs are magic and are able to execute more than one of those additions or subtractions at a time.
Myrsloik
17th June 2016, 17:41
But I think this could be some kind of SIMD, I've tested it and the results are correct, basically you are doing addition to 8 uint8_ts simultaneously as one uint64_t, why is that undefined behavior?
(I don't know shit about asm, c/c++ are the lowest level language I can take, I know avx could do this but I don't know how to do that...)
I get 22/104 as the speeds on my 5820k.
Like jackoneill says, cpus execute many instructions in parallel. You are correct that your second code block expands into about 8x as many actual instructions. But to explain the speed difference I'm just going to guess that you're memory bandwidth limited (L1 cache). Not even SIMD will help you here. Noticably that is. Autovectorization and optimizations only make it marginally faster but that could also be because it optimized away a few more instructions at the same time.
The generated code if anyone else wants to speculate:
Disassembly for your fast part. Compiled as x64 with VS2015 update 2
for (auto i = 0ull; i < 10000000ull; ++i) {
00007FF6ED711103 mov qword ptr [rsp+48h],0
00007FF6ED71110C jmp main+0CBh (07FF6ED71111Bh)
00007FF6ED71110E mov rax,qword ptr [rsp+48h]
00007FF6ED711113 inc rax
00007FF6ED711116 mov qword ptr [rsp+48h],rax
00007FF6ED71111B cmp qword ptr [rsp+48h],989680h
00007FF6ED711124 jae main+10Bh (07FF6ED71115Bh)
fake_a += fake_b;
00007FF6ED711126 mov rax,qword ptr [fake_a]
00007FF6ED71112B mov rax,qword ptr [rax]
00007FF6ED71112E mov rcx,qword ptr [fake_b]
00007FF6ED711133 add rax,qword ptr [rcx]
00007FF6ED711136 mov rcx,qword ptr [fake_a]
00007FF6ED71113B mov qword ptr [rcx],rax
fake_a -= fake_b;
00007FF6ED71113E mov rax,qword ptr [fake_a]
00007FF6ED711143 mov rcx,qword ptr [fake_b]
00007FF6ED711148 mov rcx,qword ptr [rcx]
00007FF6ED71114B mov rax,qword ptr [rax]
00007FF6ED71114E sub rax,rcx
00007FF6ED711151 mov rcx,qword ptr [fake_a]
00007FF6ED711156 mov qword ptr [rcx],rax
}
00007FF6ED711159 jmp main+0BEh (07FF6ED71110Eh)
Slow loop, abbreviated because otherwise it's too long.
for (auto i = 0ull; i < 10000000ull; ++i) {
00007FF6ED71117D cmp qword ptr [rsp+50h],989680h
00007FF6ED711186 jae main+3F1h (07FF6ED711441h)
a[0] += b[0];
00007FF6ED71118C mov eax,1
00007FF6ED711191 imul rax,rax,0
00007FF6ED711195 mov ecx,1
00007FF6ED71119A imul rcx,rcx,0
00007FF6ED71119E movzx ecx,byte ptr b[rcx]
00007FF6ED7111A3 movzx eax,byte ptr a[rax]
00007FF6ED7111A8 add eax,ecx
00007FF6ED7111AA mov ecx,1
00007FF6ED7111AF imul rcx,rcx,0
00007FF6ED7111B3 mov byte ptr a[rcx],al
a[1] += b[1];
00007FF6ED7111B7 mov eax,1
00007FF6ED7111BC imul rax,rax,1
00007FF6ED7111C0 mov ecx,1
00007FF6ED7111C5 imul rcx,rcx,1
00007FF6ED7111C9 movzx ecx,byte ptr b[rcx]
00007FF6ED7111CE movzx eax,byte ptr a[rax]
00007FF6ED7111D3 add eax,ecx
00007FF6ED7111D5 mov ecx,1
00007FF6ED7111DA imul rcx,rcx,1
00007FF6ED7111DE mov byte ptr a[rcx],al
a[2] += b[2];
00007FF6ED7111E2 mov eax,1
00007FF6ED7111E7 imul rax,rax,2
00007FF6ED7111EB mov ecx,1
00007FF6ED7111F0 imul rcx,rcx,2
00007FF6ED7111F4 movzx ecx,byte ptr b[rcx]
00007FF6ED7111F9 movzx eax,byte ptr a[rax]
00007FF6ED7111FE add eax,ecx
00007FF6ED711200 mov ecx,1
00007FF6ED711205 imul rcx,rcx,2
00007FF6ED711209 mov byte ptr a[rcx],al
a[3] += b[3];
00007FF6ED71120D mov eax,1
00007FF6ED711212 imul rax,rax,3
00007FF6ED711216 mov ecx,1
00007FF6ED71121B imul rcx,rcx,3
00007FF6ED71121F movzx ecx,byte ptr b[rcx]
00007FF6ED711224 movzx eax,byte ptr a[rax]
00007FF6ED711229 add eax,ecx
00007FF6ED71122B mov ecx,1
00007FF6ED711230 imul rcx,rcx,3
00007FF6ED711234 mov byte ptr a[rcx],al
a[4] += b[4];
...
}
00007FF6ED71143C jmp main+120h (07FF6ED711170h)
LoRd_MuldeR
18th June 2016, 13:14
I would think that cache (or even memory) cannot be a bottleneck here, because he does all computations "in place" on two local variables with total size 32 byte. This all should fit into the registers.
Myrsloik
18th June 2016, 13:18
I would think that cache (or even memory) cannot be a bottleneck here, because he does all computations "in place" on two local variables with total size 32 byte. This all should fit into the registers.
It doesn't put them in registers. Someone disabled optimizations so it loads and stores to memory. Every time.
vBulletin® v3.8.11, Copyright ©2000-2026, vBulletin Solutions Inc.