How does benchmark tool compute b.N?

834 views
Skip to first unread message

Stefan Nilsson

unread,
Jan 18, 2011, 12:32:23 PM1/18/11
to golan...@googlegroups.com
In my experience, the following type of code can time a very long time to run:

func BenchmarkCheap(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
s := Expensive()
b.StartTimer()

s.Cheap()
}
}

Is this because the initial phase that computes b.N stops the timer? If so, how about keeping the timer turned on while computing b.N? It seems that would give a better estimate of the expected running time of the test, without interfering with the accuracy of the result.

Russ Cox

unread,
Jan 18, 2011, 12:58:16 PM1/18/11
to golan...@googlegroups.com
StopTimer and StartTimer are for excluding startup costs
from the benchmark, not per-iteration costs.

func BenchmarkCheap(b *testing.B) {


b.StopTimer()
s := Expensive()
b.StartTimer()
for i := 0; i < b.N; i++ {

s.Cheap()
}
}

A loop body like the one you have will never have
an accurate result, no matter how many times the
benchmark runs the loop - because Expensive is
dominating everything - so there's little point in
adjusting the way the benchmark chooses N.

Russ

snilsson

unread,
Jan 18, 2011, 1:17:32 PM1/18/11
to golang-dev
What if you want to benchmark a method s.Clear() that resets a data
structure? The first call to Clear() is expensive, further calls on
the same s are trivial.

Russ Cox

unread,
Jan 18, 2011, 1:32:19 PM1/18/11
to snilsson, golang-dev

Including the "timer off" operations in the
computation of b.N will mean that the benchmarks
take too small a b.N.

I think your use case is just not supported by
the benchmark framework. In fact it's hard to
see how it would be supported by any benchmark
framework unless you used cycle counters or
something like that.

Russ

snilsson

unread,
Jan 18, 2011, 1:46:13 PM1/18/11
to golang-dev
Maybe I'm missing something here, but isn't the purpose of b.N to keep
the total running time of the test within reasonable limits? Wouldn't
any value of b.N make for a valid test case? (Assuming you keep b.N
reasonably large to keep variation under control.) Also, I don't see
how Expensive() could dominate the cost if you turn the timer off
during the call to this method.

Russ Cox

unread,
Jan 18, 2011, 2:12:05 PM1/18/11
to snilsson, golang-dev

The point of b.N is to make the timed test take long enough
to get a meaningful measurement; then you divide by b.N
to get the per-operation time. Right now that means you
increase b.N until the loop takes >1 second.

In this case:

func BenchmarkCheap(b *testing.B) {
b.StopTimer()
s := Expensive()
b.StartTimer()
for i := 0; i < b.N; i++ {
s.Cheap()
}
}

if Expensive takes more than a second, then you
must exclude that time from the decision about
whether N is large enough. If you include it - as your
original mail suggested - then it will choose N==1,
which is not good enough to get an accurate measurement
when the timer is off for most of the test.

Also StartTimer and StopTimer have a cost too.
If you call them N times, then you have to worry
about compensating for that.

Russ

Stefan Nilsson

unread,
Jan 18, 2011, 3:15:42 PM1/18/11
to golan...@googlegroups.com, snilsson
OK, we are talking about different time scales! I'm thinking about measuring something that takes considerably longer than b.StopTimer(); b.StartTimer(). Let's say, removing all the element from a heap of size 1<<16 by calling Pop repeatedly. (To be careful one could measure the time to stop and start the timer and subtract that from the result.)

Compound measurements like that can often be just as interesting as measuring a small operation in isolation. Is that type of testing feasible within the current framework? If not, is that something that might be added down the road?

Russ Cox

unread,
Jan 18, 2011, 3:19:39 PM1/18/11
to golan...@googlegroups.com, snilsson
On Tue, Jan 18, 2011 at 15:15, Stefan Nilsson <snil...@nada.kth.se> wrote:
> OK, we are talking about different time scales! I'm thinking about measuring
> something that takes considerably longer than b.StopTimer(); b.StartTimer().
> Let's say, removing all the element from a heap of size 1<<16 by calling Pop
> repeatedly. (To be careful one could measure the time to stop and start the
> timer and subtract that from the result.)

One could try that, but the whole point of doing things
N times for some large N is that the tiny operations
tend to be smaller than the resolution of the timer,
StopTimer and StartTimer included. It is far better
not to call them in the loop.

In the case you gave I would instead make N the
number of items to Pop.

Russ

snilsson

unread,
Jan 18, 2011, 3:44:51 PM1/18/11
to golang-dev
On Jan 18, 9:19 pm, Russ Cox <r...@golang.org> wrote:
> One could try that, but the whole point of doing things
> N times for some large N is that the tiny operations
> tend to be smaller than the resolution of the timer,
> StopTimer and StartTimer included.  It is far better
> not to call them in the loop.

It's not that big a problem. On the machine in front of me, stopping
and starting the timer takes 200ns, while emptying a size 1<<16 heap
takes 200μs. However, it is somewhat inconvenient that the estimate of
N tends to be too large, resulting in long running times.


> In the case you gave I would instead make N the
> number of items to Pop.

That's like the chicken and the egg, isn't it. Before entering the
loop you don't know N. Also, if you don't nail N exactly you will
measure popping an empty heap for some of the iterations.

Russ Cox

unread,
Jan 18, 2011, 4:06:24 PM1/18/11
to snilsson, golang-dev
> It's not that big a problem. On the machine in front of me, stopping
> and starting the timer takes 200ns, while emptying a size 1<<16 heap
> takes 200μs. However, it is somewhat inconvenient that the estimate of
> N tends to be too large, resulting in long running times.
>
>> In the case you gave I would instead make N the
>> number of items to Pop.
>
> That's like the chicken and the egg, isn't it. Before entering the
> loop you don't know N. Also, if you don't nail N exactly you will
> measure popping an empty heap for some of the iterations.

No.

package main

import (
"container/heap"
"container/vector"
"fmt"
"rand"
"testing"
)

type v struct {
vector.IntVector
}

func (v *v) Push(x interface{}) {
v.IntVector.Push(x.(int))
}

func (v *v) Pop() interface{} {
return v.IntVector.Pop()
}

var h v

func BenchmarkHeap(b *testing.B) {
b.StopTimer()


for i := 0; i < b.N; i++ {

heap.Push(&h, rand.Int())


}
b.StartTimer()
for i := 0; i < b.N; i++ {

heap.Pop(&h)
}
}

func main() {
fmt.Println(testing.Benchmark(BenchmarkHeap))
}

snilsson

unread,
Jan 18, 2011, 4:46:54 PM1/18/11
to golang-dev
Thanks, that's a neat idea! A funny side effect is that the faster I
make my priority queue, the more memory the benchmark eats! But I can
live with that. :)

Once again, thanks.
Reply all
Reply to author
Forward
0 new messages