Data structures
The code is in
hands-on/memory/datastructures.
Exercise 1: the particle struct
Modify Particle.h: give the struct GoodParticle these members:
- one
std::stringfor the particle’s name; - three
doubles for the x, y, z coordinates; - three
bools to mark a collision along the x, y, z directions; - one
floatfor the mass; - one
floatfor the energy; - three
doubles for the px, py, pz momenta; - one
intfor the particle’s id.
What is the best order for the members?
Compile and run size_of_particle.cc to check the size of your GoodParticle:
g++ -std=c++20 -Wall -O2 size_of_particle.cc -o size_of_particle
./size_of_particle
- What is the smallest size you can get?
- How many cache lines does one particle take?
Exercise 2: the access pattern
Now use GoodParticle for a computation, in aos_soa.cc.
- Write a function that initialises a collection of
NGoodParticles, with every member set.mainalready generates the random values; every particle starts inside the box, between 0 andx_max. - Write a function that takes the collection, a time interval
tandx_max. For every particle:- update the position:
x = x + px / m * t; - if the new
xis below 0 or abovex_max, sethit_xto true and change the sign ofpx; - otherwise set
hit_xto false.
Repeat this
NItertimes (tis the same for all particles and iterations). - update the position:
How are you accessing the members of the data structure? Of all the bytes of a particle, how many does this loop use?
Exercise 3: AoS vs SoA
Modify Particle.h: write the class ParticleSoA, with the same members as GoodParticle
but one std::vector per member (for example std::vector<double> x_).
- Allocate all the vectors for
Nparticles once, in the constructor, so that the computation never has to resize them. - Write the two functions of Exercise 2 for the SoA.
- Measure the time of the AoS and of the SoA version and compare them.
g++ -std=c++20 -Wall -O2 aos_soa.cc -o aos_soa
./aos_soa
- Compile with
-O0,-O1,-O2and-O3. What happens? - How many bytes does each iteration read from memory in the two versions?
- How does the interface of SoA change with respect to AoS?
Bonus: matrix multiplication
matrix.cc multiplies two N × N matrices, stored row by row, in two ways: the naive loop, and
a version that transposes B first.
g++ -std=c++20 -Wall -O3 -g matrix.cc -o matrix
./matrix
- Which one is faster, and by how much? In the inner loop of each, which elements of B are read one after the other, and how far apart are they in memory?
-
Look at the caches. Comment out one of the two methods in
main, compile again, and run:perf stat -e cycles,instructions,L1-dcache-loads,L1-dcache-load-misses,cache-misses ./matrixThen do the same for the other method. How many L1 misses are there per multiplication? (The perf page explains these counters.)
- Instead of transposing, swap the two inner loops of the naive version (
i, k, jinstead ofi, j, k). Is the result still correct? Is it faster?