Re: Help requested: Interpreting results from bench.pl
[email protected] (Tony Cook) Fri, 13 Mar 2026 09:30:52 +1100
| Newsgroups | perl.perl5.porters |
|---|---|
| Message-ID | <[email protected]> |
On Thu, Mar 12, 2026 at 10:48:34AM -0600, Karl Williamson via perl5-porters wrote:
> I've been told, and it may even be documented that the rows below IND_m are
> not significant; so don't even bother looking at them. And it might be rows
> below plain 'IND' that aren't important.
My problem is that none of the values is a good direct indicator of
performance.
For reference, see the matrix multiplication example below.
Without arguments it runs the "dumb" implementation:
$ perf stat ./matmult 2>&1 | grep task-clock
667.59 msec task-clock # 0.999 CPUs utilized
$ perf stat ./matmult smart 2>&1 | grep task-clock
373.19 msec task-clock # 0.998 CPUs utilized
$ valgrind --tool=cachegrind --cache-sim=yes ./matmult
==4011921== I refs: 6,477,837,655
==4011921== I1 misses: 1,249
==4011921== LLi misses: 1,232
==4011921== I1 miss rate: 0.00%
==4011921== LLi miss rate: 0.00%
==4011921==
==4011921== D refs: 2,438,485,653 (1,605,672,547 rd + 832,813,106 wr)
==4011921== D1 misses: 101,572,718 ( 101,062,170 rd + 510,548 wr)
==4011921== LLd misses: 16,384 ( 1,019 rd + 15,365 wr)
==4011921== D1 miss rate: 4.2% ( 6.3% + 0.1% )
==4011921== LLd miss rate: 0.0% ( 0.0% + 0.0% )
==4011921==
==4011921== LL refs: 101,573,967 ( 101,063,419 rd + 510,548 wr)
==4011921== LL misses: 17,616 ( 2,251 rd + 15,365 wr)
==4011921== LL miss rate: 0.0% ( 0.0% + 0.0% )
$ valgrind --tool=cachegrind --cache-sim=yes ./matmult smart
==4011954== I refs: 6,473,797,069
==4011954== I1 misses: 1,248
==4011954== LLi misses: 1,231
==4011954== I1 miss rate: 0.00%
==4011954== LLi miss rate: 0.00%
==4011954==
==4011954== D refs: 3,234,445,061 (2,401,632,355 rd + 832,812,706 wr)
==4011954== D1 misses: 101,532,923 ( 101,022,370 rd + 510,553 wr)
==4011954== LLd misses: 16,387 ( 1,019 rd + 15,368 wr)
==4011954== D1 miss rate: 3.1% ( 4.2% + 0.1% )
==4011954== LLd miss rate: 0.0% ( 0.0% + 0.0% )
==4011954==
==4011954== LL refs: 101,534,171 ( 101,023,618 rd + 510,553 wr)
==4011954== LL misses: 17,618 ( 2,250 rd + 15,368 wr)
==4011954== LL miss rate: 0.0% ( 0.0% + 0.0% )
Note that while the "smart" implementation is about twice as fast, the
cachegrind results are almost the same.
Asking perf stat for more details reveals the cause:
$ perf stat -d ./matmult
...
1,603,083,228 L1-dcache-loads # 2.392 G/sec (62.52%)
104,022,669 L1-dcache-load-misses # 6.49% of all L1-dcache accesses (62.65%)
52,203,679 LLC-loads # 77.900 M/sec (50.12%)
571 LLC-load-misses # 0.00% of all LL-cache accesses (50.02%)
$ perf stat -d ./matmult smart
2,390,381,021 L1-dcache-loads # 6.496 G/sec (64.13%)
101,252,883 L1-dcache-load-misses # 4.24% of all L1-dcache accesses (63.31%)
1,816,176 LLC-loads # 4.935 M/sec (49.18%)
598 LLC-load-misses # 0.03% of all LL-cache accesses (48.09%)
(perf stat highlights the L1-dcache-load-misses in the first result.)
So while cachegrind (which is what Porting/bench.pl uses) provides
useful information for tracking down performance issue causes, I don't
think it's so useful for benchmarking.
The reproducibility of the valgrind results is really tempting, but I
don't really trust it to reflect performance.
> There are people who say that this whole cachegrind output is not relevant
> to modern processors. I plead ignorance.
https://valgrind.org/docs/manual/cg-manual.html#cache-sim
"The cache simulation approximates the hardware of an AMD Athlon CPU
circa 2002"
https://valgrind.org/docs/manual/cg-manual.html#branch-sim
"Cachegrind simulates branch predictors intended to be typical of
mainstream desktop/server processors of around 2004."
Tony
===
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
// only square because I'm lazy
size_t d;
double *values;
} matrix;
matrix *
new_matrix(size_t d) {
// missing error checking that should be done in production
matrix *result = malloc(sizeof(matrix));
*result = (matrix){
.d = d,
.values = malloc(sizeof(double) * d * d)
};
return result;
}
void
free_matrix(matrix *m) {
free(m->values);
free(m);
}
void
clear_matrix(matrix *m) {
size_t limit = m->d * m->d;
double *values = m->values;
for (size_t i = 0; i < limit; ++i)
values[i] = 0.0;
}
#define M(matrix, row, column) \
((matrix)->values[(row) * (matrix)->d + (column)])
matrix *
random_matrix(size_t d) {
matrix *m = new_matrix(d);
size_t limit = d * d;
for (size_t i = 0; i < limit; ++i)
m->values[i] = drand48();
return m;
}
// multiply a by b result in c
// typical cache unfriendly implementation
void
matmult_dumb(matrix *a, matrix *b, matrix *c) {
size_t d = c->d;
clear_matrix(c);
for (size_t i = 0; i < d; ++i) {
for (size_t j = 0; j < d; ++j) {
for (size_t k = 0; k < d; ++k) {
M(c, i, j) += M(a, i, k) * M(b, k, j);
}
}
}
}
// multiply a by b result in c
void
matmult_smart(matrix *a, matrix *b, matrix *c) {
size_t d = c->d;
clear_matrix(c);
for (size_t i = 0; i < d; ++i) {
for (size_t k = 0; k < d; ++k) {
for (size_t j = 0; j < d; ++j) {
M(c, i, j) += M(a, i, k) * M(b, k, j);
}
}
}
}
#define MSIZE 200
int
main(int argc, char **argv) {
bool smart = argc > 1;
matrix *c = new_matrix(MSIZE);
matrix *a = random_matrix(MSIZE);
matrix *b = random_matrix(MSIZE);
if (smart) {
for (int i = 0; i < 100; ++i) {
matmult_smart(a, b, c);
}
}
else {
for (int i = 0; i < 100; ++i) {
matmult_dumb(a, b, c);
}
}
free_matrix(c);
free_matrix(a);
free_matrix(b);
}