Skip to content

Commit 93cbfb2

Browse files
committed
bench: benchmark soa structs
1 parent e462e2a commit 93cbfb2

1 file changed

Lines changed: 378 additions & 0 deletions

File tree

‎bench/bench_soa_structs.v‎

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
// bench_soa_structs.v - V2 benchmark for @[soa] structs
2+
// Build and run with:
3+
// ./cmd/v2/v2 -prod -backend cleanc bench/bench_soa_structs.v -o bench/bench_soa_structs
4+
// ./bench/bench_soa_structs
5+
module main
6+
7+
import time
8+
9+
const particle_count = 500_000
10+
const build_rounds = 5
11+
const sum_rounds = 48
12+
const hot_fields_rounds = 40
13+
const full_struct_rounds = 20
14+
const integrate_rounds = 20
15+
16+
@[soa]
17+
struct Particle {
18+
x f32
19+
y f32
20+
z f32
21+
vx f32
22+
vy f32
23+
vz f32
24+
mass f32
25+
temperature f32
26+
ax f32
27+
ay f32
28+
az f32
29+
life f32
30+
drag f32
31+
color_r f32
32+
color_g f32
33+
color_b f32
34+
}
35+
36+
@[typedef]
37+
struct C.Particle_SOA {
38+
len int
39+
cap int
40+
x &f32
41+
y &f32
42+
z &f32
43+
vx &f32
44+
vy &f32
45+
vz &f32
46+
mass &f32
47+
temperature &f32
48+
ax &f32
49+
ay &f32
50+
az &f32
51+
life &f32
52+
drag &f32
53+
color_r &f32
54+
color_g &f32
55+
color_b &f32
56+
}
57+
58+
fn C.Particle_SOA_new(int, int) C.Particle_SOA
59+
fn C.Particle_SOA_push(&C.Particle_SOA, Particle)
60+
fn C.Particle_SOA_get(C.Particle_SOA, int) Particle
61+
fn C.Particle_SOA_free(&C.Particle_SOA)
62+
63+
struct BenchResult {
64+
elapsed_ms i64
65+
checksum f64
66+
}
67+
68+
fn make_particle(i int) Particle {
69+
base := f32(i % 1024)
70+
return Particle{
71+
x: base * 0.25
72+
y: base * 0.5
73+
z: base * 0.75
74+
vx: f32(0.001) * f32((i % 13) + 1)
75+
vy: f32(0.0015) * f32((i % 17) + 1)
76+
vz: f32(0.0005) * f32((i % 19) + 1)
77+
mass: f32(1.0) + f32(i % 5) * f32(0.1)
78+
temperature: f32(20.0) + f32(i % 7)
79+
ax: f32(0.0001) * f32((i % 11) + 1)
80+
ay: f32(0.0002) * f32((i % 9) + 1)
81+
az: f32(0.0003) * f32((i % 7) + 1)
82+
life: f32(100.0) - f32(i % 80)
83+
drag: f32(0.98)
84+
color_r: f32(i % 255) / f32(255.0)
85+
color_g: f32((i * 3) % 255) / f32(255.0)
86+
color_b: f32((i * 7) % 255) / f32(255.0)
87+
}
88+
}
89+
90+
fn build_aos() []Particle {
91+
mut particles := []Particle{cap: particle_count}
92+
for i in 0 .. particle_count {
93+
particles << make_particle(i)
94+
}
95+
return particles
96+
}
97+
98+
fn build_soa() C.Particle_SOA {
99+
mut soa := C.Particle_SOA_new(0, particle_count)
100+
for i in 0 .. particle_count {
101+
C.Particle_SOA_push(&soa, make_particle(i))
102+
}
103+
return soa
104+
}
105+
106+
fn build_soa_indexed() C.Particle_SOA {
107+
mut soa := C.Particle_SOA_new(particle_count, particle_count)
108+
unsafe {
109+
for i in 0 .. particle_count {
110+
p := make_particle(i)
111+
soa.x[i] = p.x
112+
soa.y[i] = p.y
113+
soa.z[i] = p.z
114+
soa.vx[i] = p.vx
115+
soa.vy[i] = p.vy
116+
soa.vz[i] = p.vz
117+
soa.mass[i] = p.mass
118+
soa.temperature[i] = p.temperature
119+
soa.ax[i] = p.ax
120+
soa.ay[i] = p.ay
121+
soa.az[i] = p.az
122+
soa.life[i] = p.life
123+
soa.drag[i] = p.drag
124+
soa.color_r[i] = p.color_r
125+
soa.color_g[i] = p.color_g
126+
soa.color_b[i] = p.color_b
127+
}
128+
}
129+
return soa
130+
}
131+
132+
fn bench_build_aos() BenchResult {
133+
sw := time.new_stopwatch()
134+
mut sum := f64(0.0)
135+
for _ in 0 .. build_rounds {
136+
particles := build_aos()
137+
sum += particles.len
138+
sum += particles[0].x
139+
sum += particles[particles.len - 1].life
140+
}
141+
return BenchResult{
142+
elapsed_ms: sw.elapsed().milliseconds()
143+
checksum: sum
144+
}
145+
}
146+
147+
fn bench_build_soa() BenchResult {
148+
sw := time.new_stopwatch()
149+
mut sum := f64(0.0)
150+
for _ in 0 .. build_rounds {
151+
mut soa := build_soa()
152+
sum += soa.len
153+
unsafe {
154+
sum += soa.x[0]
155+
sum += soa.life[soa.len - 1]
156+
}
157+
C.Particle_SOA_free(&soa)
158+
}
159+
return BenchResult{
160+
elapsed_ms: sw.elapsed().milliseconds()
161+
checksum: sum
162+
}
163+
}
164+
165+
fn bench_build_soa_indexed() BenchResult {
166+
sw := time.new_stopwatch()
167+
mut sum := f64(0.0)
168+
for _ in 0 .. build_rounds {
169+
mut soa := build_soa_indexed()
170+
sum += soa.len
171+
unsafe {
172+
sum += soa.x[0]
173+
sum += soa.life[soa.len - 1]
174+
}
175+
C.Particle_SOA_free(&soa)
176+
}
177+
return BenchResult{
178+
elapsed_ms: sw.elapsed().milliseconds()
179+
checksum: sum
180+
}
181+
}
182+
183+
fn bench_sum_x_aos(particles []Particle) BenchResult {
184+
sw := time.new_stopwatch()
185+
mut sum := f64(0.0)
186+
for _ in 0 .. sum_rounds {
187+
for i in 0 .. particles.len {
188+
sum += particles[i].x
189+
}
190+
}
191+
return BenchResult{
192+
elapsed_ms: sw.elapsed().milliseconds()
193+
checksum: sum
194+
}
195+
}
196+
197+
fn bench_sum_x_soa(soa C.Particle_SOA) BenchResult {
198+
sw := time.new_stopwatch()
199+
mut sum := f64(0.0)
200+
for _ in 0 .. sum_rounds {
201+
unsafe {
202+
for i in 0 .. soa.len {
203+
sum += soa.x[i]
204+
}
205+
}
206+
}
207+
return BenchResult{
208+
elapsed_ms: sw.elapsed().milliseconds()
209+
checksum: sum
210+
}
211+
}
212+
213+
fn bench_sum_hot_fields_aos(particles []Particle) BenchResult {
214+
sw := time.new_stopwatch()
215+
mut sum := f64(0.0)
216+
for _ in 0 .. hot_fields_rounds {
217+
for i in 0 .. particles.len {
218+
sum += particles[i].x + particles[i].y + particles[i].z + particles[i].life
219+
}
220+
}
221+
return BenchResult{
222+
elapsed_ms: sw.elapsed().milliseconds()
223+
checksum: sum
224+
}
225+
}
226+
227+
fn bench_sum_hot_fields_soa(soa C.Particle_SOA) BenchResult {
228+
sw := time.new_stopwatch()
229+
mut sum := f64(0.0)
230+
for _ in 0 .. hot_fields_rounds {
231+
unsafe {
232+
for i in 0 .. soa.len {
233+
sum += soa.x[i] + soa.y[i] + soa.z[i] + soa.life[i]
234+
}
235+
}
236+
}
237+
return BenchResult{
238+
elapsed_ms: sw.elapsed().milliseconds()
239+
checksum: sum
240+
}
241+
}
242+
243+
fn bench_sum_all_fields_aos(particles []Particle) BenchResult {
244+
sw := time.new_stopwatch()
245+
mut sum := f64(0.0)
246+
for _ in 0 .. full_struct_rounds {
247+
for i in 0 .. particles.len {
248+
sum += particles[i].x + particles[i].y + particles[i].z + particles[i].vx +
249+
particles[i].vy + particles[i].vz + particles[i].mass + particles[i].temperature +
250+
particles[i].ax + particles[i].ay + particles[i].az + particles[i].life +
251+
particles[i].drag + particles[i].color_r + particles[i].color_g +
252+
particles[i].color_b
253+
}
254+
}
255+
return BenchResult{
256+
elapsed_ms: sw.elapsed().milliseconds()
257+
checksum: sum
258+
}
259+
}
260+
261+
fn bench_sum_all_fields_soa(soa C.Particle_SOA) BenchResult {
262+
sw := time.new_stopwatch()
263+
mut sum := f64(0.0)
264+
for _ in 0 .. full_struct_rounds {
265+
for i in 0 .. soa.len {
266+
p := C.Particle_SOA_get(soa, i)
267+
sum += p.x + p.y + p.z + p.vx + p.vy + p.vz + p.mass + p.temperature + p.ax + p.ay +
268+
p.az + p.life + p.drag + p.color_r + p.color_g + p.color_b
269+
}
270+
}
271+
return BenchResult{
272+
elapsed_ms: sw.elapsed().milliseconds()
273+
checksum: sum
274+
}
275+
}
276+
277+
fn bench_integrate_aos(mut particles []Particle) BenchResult {
278+
sw := time.new_stopwatch()
279+
for _ in 0 .. integrate_rounds {
280+
for i in 0 .. particles.len {
281+
particles[i].vx += particles[i].ax
282+
particles[i].vy += particles[i].ay
283+
particles[i].vz += particles[i].az
284+
particles[i].x += particles[i].vx * particles[i].mass
285+
particles[i].y += particles[i].vy * particles[i].mass
286+
particles[i].z += particles[i].vz * particles[i].mass
287+
particles[i].life -= particles[i].drag
288+
}
289+
}
290+
mut sum := f64(0.0)
291+
for i in 0 .. particles.len {
292+
sum += particles[i].x + particles[i].y + particles[i].z + particles[i].life
293+
}
294+
return BenchResult{
295+
elapsed_ms: sw.elapsed().milliseconds()
296+
checksum: sum
297+
}
298+
}
299+
300+
fn bench_integrate_soa(mut soa C.Particle_SOA) BenchResult {
301+
sw := time.new_stopwatch()
302+
unsafe {
303+
for _ in 0 .. integrate_rounds {
304+
for i in 0 .. soa.len {
305+
soa.vx[i] += soa.ax[i]
306+
soa.vy[i] += soa.ay[i]
307+
soa.vz[i] += soa.az[i]
308+
soa.x[i] += soa.vx[i] * soa.mass[i]
309+
soa.y[i] += soa.vy[i] * soa.mass[i]
310+
soa.z[i] += soa.vz[i] * soa.mass[i]
311+
soa.life[i] -= soa.drag[i]
312+
}
313+
}
314+
}
315+
mut sum := f64(0.0)
316+
unsafe {
317+
for i in 0 .. soa.len {
318+
sum += soa.x[i] + soa.y[i] + soa.z[i] + soa.life[i]
319+
}
320+
}
321+
return BenchResult{
322+
elapsed_ms: sw.elapsed().milliseconds()
323+
checksum: sum
324+
}
325+
}
326+
327+
fn print_result(name string, aos BenchResult, soa BenchResult) {
328+
println(name)
329+
println(' aos: ${aos.elapsed_ms} ms, checksum=${aos.checksum}')
330+
println(' soa: ${soa.elapsed_ms} ms, checksum=${soa.checksum}')
331+
if soa.elapsed_ms > 0 {
332+
println(' speedup: ${f64(aos.elapsed_ms) / f64(soa.elapsed_ms)}x')
333+
}
334+
}
335+
336+
fn print_build_results(aos BenchResult, soa_push BenchResult, soa_indexed BenchResult) {
337+
println('build particles')
338+
println(' aos: ${aos.elapsed_ms} ms, checksum=${aos.checksum}')
339+
println(' soa push: ${soa_push.elapsed_ms} ms, checksum=${soa_push.checksum}')
340+
println(' soa indexed: ${soa_indexed.elapsed_ms} ms, checksum=${soa_indexed.checksum}')
341+
if soa_indexed.elapsed_ms > 0 {
342+
println(' push vs indexed: ${f64(soa_push.elapsed_ms) / f64(soa_indexed.elapsed_ms)}x')
343+
println(' indexed speedup over aos: ${f64(aos.elapsed_ms) / f64(soa_indexed.elapsed_ms)}x')
344+
}
345+
}
346+
347+
fn main() {
348+
println('soa struct bench (v2 cleanc)')
349+
println('particle_count=${particle_count}, build_rounds=${build_rounds}, sum_rounds=${sum_rounds}, hot_fields_rounds=${hot_fields_rounds}, full_struct_rounds=${full_struct_rounds}, integrate_rounds=${integrate_rounds}')
350+
351+
result_build_aos := bench_build_aos()
352+
result_build_soa_push := bench_build_soa()
353+
result_build_soa_indexed := bench_build_soa_indexed()
354+
print_build_results(result_build_aos, result_build_soa_push, result_build_soa_indexed)
355+
356+
aos_scan := build_aos()
357+
soa_scan := build_soa()
358+
result_aos_scan := bench_sum_x_aos(aos_scan)
359+
result_soa_scan := bench_sum_x_soa(soa_scan)
360+
print_result('sum x only', result_aos_scan, result_soa_scan)
361+
362+
result_aos_hot_fields := bench_sum_hot_fields_aos(aos_scan)
363+
result_soa_hot_fields := bench_sum_hot_fields_soa(soa_scan)
364+
print_result('sum x/y/z/life', result_aos_hot_fields, result_soa_hot_fields)
365+
366+
result_aos_full_struct := bench_sum_all_fields_aos(aos_scan)
367+
result_soa_full_struct := bench_sum_all_fields_soa(soa_scan)
368+
print_result('materialize full struct and sum all fields', result_aos_full_struct,
369+
result_soa_full_struct)
370+
C.Particle_SOA_free(&soa_scan)
371+
372+
mut aos_integrate := build_aos()
373+
mut soa_integrate := build_soa()
374+
result_aos_integrate := bench_integrate_aos(mut aos_integrate)
375+
result_soa_integrate := bench_integrate_soa(mut soa_integrate)
376+
print_result('integrate position, velocity, and life', result_aos_integrate, result_soa_integrate)
377+
C.Particle_SOA_free(&soa_integrate)
378+
}

0 commit comments

Comments
 (0)