The following code snips does NOT create a level of indirection. You can see it by introspect the assembler code.
So don't worry, start structure your data. See the next post for a practice example.
Example 1
Lets begin with a simple struct contains 3 members: double, float, int. Sum them up. And see what the compile generate
struct A { double d; float f; int i; }; static_assert(sizeof(A) == 16); auto func(const A& a){ return a.d + a.f + a.i; }
With the static assert sizeof you can be sure there is no padding in the data structure. The generate ASM code compiled with GCC 8.1 -O1 I comment it to make it easier for you.
func(A const&): pxor %xmm0, %xmm0 # xmm0 = 0 cvtss2sd 8(%rdi), %xmm0 # convert float to double and store it in xmm0 addsd (%rdi), %xmm0 # add double and the converted float to xmm0 pxor %xmm1, %xmm1 # xmm1 = 0 cvtsi2sd 12(%rdi), %xmm1 # convert integer to double and store it in xmm1 addsd %xmm1, %xmm0 # add the already added float+double to the converted integer to xmm0 ret # return xmm0 as result
You can see two conversions from int/float to double and two additions. You can also see the offsets. The double value has offset 0, the float value 8 bytes and the integer value (8+4)=12 bytes. This is quite straight. So add more code.
Example 2
This example create a nested struct Bdata inside struct B. Sum the values up again and see what the compiler generate.
struct B { struct Bdata { double d; float f; int i; }; Bdata b; }; static_assert(sizeof(B) == 16); auto func2(const B& a){ return a.b.d + a.b.f + a.b.i; }
It is exactly, 100%, the same assembler code as example1 ! So i don't show it here again. The memory layout of struct B is the same as struct A, so the same ASM code is generated. No overhead by the extra variable b.
Example 3
In the next example a std::tuple is used to store the 3 values. The access is provided with the function std::get. It looks different. But wait...
struct C { std::tuple<double, float, int> c; }; static_assert(sizeof(C) == 16); auto func3(const C& a){ return std::get<0>(a.c) + std::get<1>(a.c) + std::get<2>(a.c); }
This time, the generated code is different
func3(C const&): pxor %xmm0, %xmm0 # xmm0 = 0 cvtss2sd 4(%rdi), %xmm0 # convert float to double and store it in xmm0 addsd 8(%rdi), %xmm0 # add double and the converted float to xmm0 pxor %xmm1, %xmm1 # xmm1 = 0 cvtsi2sd (%rdi), %xmm1 # convert integer to double and store it in xmm1 addsd %xmm1, %xmm0 # add the already added float+double to the converted integer to xmm0 ret # return xmm0 as result
But if you take a closer look, it is still the same! Only the memory layout has changed. First the integer, then the float, then the double. A std::stuple store it's values in reverse order! But the generated assembler code is 100% identical to the first example!
Example 4
In the last example I change the structure a little bit. But you can guess, the result is still the same.
struct D { double d; struct Ddata { float f; int i; }; Ddata x; }; static_assert(sizeof(D) == 16); auto func4(const D& a){ return a.d + a.x.f + a.x.i; }
See the next post for a practice example.