Skip to content

Commit 2eb15c8

Browse files
authored
add benchmark
* add benchmark * Update README.md
1 parent 5646534 commit 2eb15c8

3 files changed

Lines changed: 238 additions & 1 deletion

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@
1010

1111
# Output of the go coverage tool, specifically when used with LiteIDE
1212
*.out
13+
14+
# IDE
15+
.idea/
16+
*.iml

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ An *errorx* library makes an approach to create a toolset that would help remedy
3636

3737
As a result, the goal of the library is to provide a brief, expressive syntax for a conventional error handling and to discourage usage patterns that bring more harm than they're worth.
3838

39-
Error-related, negative codepath is typically less well tested, though of, and may confuse the reader more than its positive counterpart. Therefore, an error system could do well without too much of a flexibility and unpredictability
39+
Error-related, negative codepath is typically less well tested, though of, and may confuse the reader more than its positive counterpart. Therefore, an error system could do well without too much of a flexibility and unpredictability.
4040

4141
# errorx
4242

@@ -146,6 +146,36 @@ This way, a receiver of an error always treats it the same way, and it is the pr
146146

147147
Other relevant tools include ```EnsureStackTrace(err)``` to provide an error of unknown nature with a stack trace, if it lacks one.
148148

149+
### Stack traces benchmark
150+
151+
As performance is obviously an issue, some measurements are in order. The benchmark is provided with the library. In all of benchmark cases, a very simple code is called that does nothing but grows a number of frames and immediately returns an error.
152+
153+
Result sample, MacBook Pro Intel Core i7-6920HQ CPU @ 2.90GHz 4 core:
154+
155+
name | runs | ns/op | note
156+
------ | ------: | ------: | ------
157+
BenchmarkSimpleError10 | 20000000 | 57.2 | simple error, 10 frames deep
158+
BenchmarkErrorxError10 | 10000000 | 138 | same with errorx error
159+
BenchmarkStackTraceErrorxError10 | 1000000 | 1601 | same with collected stack trace
160+
BenchmarkSimpleError100 | 3000000 | 421 | simple error, 100 frames deep
161+
BenchmarkErrorxError100 | 3000000 | 507 | same with errorx error
162+
BenchmarkStackTraceErrorxError100 | 300000 | 4450 | same with collected stack trace
163+
BenchmarkStackTraceNaiveError100-8 | 2000 | 588135 | same with naive debug.Stack() error implementation
164+
BenchmarkSimpleErrorPrint100 | 2000000 | 617 | simple error, 100 frames deep, format output
165+
BenchmarkErrorxErrorPrint100 | 2000000 | 935 | same with errorx error
166+
BenchmarkStackTraceErrorxErrorPrint100 | 30000 | 58965 | same with collected stack trace
167+
BenchmarkStackTraceNaiveErrorPrint100-8 | 2000 | 599155 | same with naive debug.Stack() error implementation
168+
169+
Key takeaways:
170+
* With deep enough call stack, trace capture brings **10x slowdown**
171+
* This is an absolute **worst case measurement, no-op function**; in a real life, much more time is spent doing actual work
172+
* Then again, in real life code invocation does not always result in error, so the overhead is proportional to the % of error returns
173+
* Still, it pays to omit stack trace collection when it would be of no use
174+
* It is actually **much more expensive to format** an error with a stack trace than to create it, roughly **another 10x**
175+
* Compared to the most naive approach to stack trace collection, error creation it is **100x** cheaper with errorx
176+
* Therefore, it is totally OK to create an error with a stack trace that would then be handled and not printed to log
177+
* Realistically, stack trace overhead is only painful either if a code is very hot (called a lot and returns errors often) or if an error is used as a control flow mechanism and does not constitute an actual problem; in both cases, stack trace should be omitted
178+
149179
## More
150180

151181
See godoc for other *errorx* features:
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package benchmark
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"runtime/debug"
7+
"testing"
8+
9+
"github.com/joomcode/errorx"
10+
)
11+
12+
var errorSink error
13+
14+
func BenchmarkSimpleError10(b *testing.B) {
15+
for n := 0; n < b.N; n++ {
16+
errorSink = function0(10, createSimpleError)
17+
}
18+
consumeResult(errorSink)
19+
}
20+
21+
func BenchmarkErrorxError10(b *testing.B) {
22+
for n := 0; n < b.N; n++ {
23+
errorSink = function0(10, createSimpleErrorxError)
24+
}
25+
consumeResult(errorSink)
26+
}
27+
28+
func BenchmarkStackTraceErrorxError10(b *testing.B) {
29+
for n := 0; n < b.N; n++ {
30+
errorSink = function0(10, createErrorxError)
31+
}
32+
consumeResult(errorSink)
33+
}
34+
35+
func BenchmarkSimpleError100(b *testing.B) {
36+
for n := 0; n < b.N; n++ {
37+
errorSink = function0(100, createSimpleError)
38+
}
39+
consumeResult(errorSink)
40+
}
41+
42+
func BenchmarkErrorxError100(b *testing.B) {
43+
for n := 0; n < b.N; n++ {
44+
errorSink = function0(100, createSimpleErrorxError)
45+
}
46+
consumeResult(errorSink)
47+
}
48+
49+
func BenchmarkStackTraceErrorxError100(b *testing.B) {
50+
for n := 0; n < b.N; n++ {
51+
errorSink = function0(100, createErrorxError)
52+
}
53+
consumeResult(errorSink)
54+
}
55+
56+
func BenchmarkStackTraceNaiveError100(b *testing.B) {
57+
for n := 0; n < b.N; n++ {
58+
errorSink = function0(100, createNaiveError)
59+
}
60+
consumeResult(errorSink)
61+
}
62+
63+
func BenchmarkSimpleErrorPrint100(b *testing.B) {
64+
for n := 0; n < b.N; n++ {
65+
err := function0(100, createSimpleError)
66+
emulateErrorPrint(err)
67+
errorSink = err
68+
}
69+
consumeResult(errorSink)
70+
}
71+
72+
func BenchmarkErrorxErrorPrint100(b *testing.B) {
73+
for n := 0; n < b.N; n++ {
74+
err := function0(100, createSimpleErrorxError)
75+
emulateErrorPrint(err)
76+
errorSink = err
77+
}
78+
consumeResult(errorSink)
79+
}
80+
81+
func BenchmarkStackTraceErrorxErrorPrint100(b *testing.B) {
82+
for n := 0; n < b.N; n++ {
83+
err := function0(100, createErrorxError)
84+
emulateErrorPrint(err)
85+
errorSink = err
86+
}
87+
consumeResult(errorSink)
88+
}
89+
90+
func BenchmarkStackTraceNaiveErrorPrint100(b *testing.B) {
91+
for n := 0; n < b.N; n++ {
92+
err := function0(100, createNaiveError)
93+
emulateErrorPrint(err)
94+
errorSink = err
95+
}
96+
consumeResult(errorSink)
97+
}
98+
99+
func createSimpleError() error {
100+
return errors.New("benchmark")
101+
}
102+
103+
var (
104+
Errors = errorx.NewNamespace("errorx.benchmark")
105+
NoStackTraceError = Errors.NewType("no_stack_trace").ApplyModifiers(errorx.TypeModifierOmitStackTrace)
106+
StackTraceError = Errors.NewType("stack_trace")
107+
)
108+
109+
func createSimpleErrorxError() error {
110+
return NoStackTraceError.New("benchmark")
111+
}
112+
113+
func createErrorxError() error {
114+
return StackTraceError.New("benchmark")
115+
}
116+
117+
type naiveError struct {
118+
stack []byte
119+
}
120+
121+
func (err naiveError) Error() string {
122+
return fmt.Sprintf("benchmark\n%s", err.stack)
123+
}
124+
125+
func createNaiveError() error {
126+
return naiveError{stack: debug.Stack()}
127+
}
128+
129+
func function0(depth int, generate func() error) error {
130+
if depth == 0 {
131+
return generate()
132+
}
133+
134+
switch depth % 3 {
135+
case 0:
136+
return function1(depth-1, generate)
137+
case 1:
138+
return function2(depth-1, generate)
139+
default:
140+
return function3(depth-1, generate)
141+
}
142+
}
143+
144+
func function1(depth int, generate func() error) error {
145+
if depth == 0 {
146+
return generate()
147+
}
148+
149+
return function4(depth-1, generate)
150+
}
151+
152+
func function2(depth int, generate func() error) error {
153+
if depth == 0 {
154+
return generate()
155+
}
156+
157+
return function4(depth-1, generate)
158+
}
159+
160+
func function3(depth int, generate func() error) error {
161+
if depth == 0 {
162+
return generate()
163+
}
164+
165+
return function4(depth-1, generate)
166+
}
167+
168+
func function4(depth int, generate func() error) error {
169+
switch depth {
170+
case 0:
171+
return generate()
172+
default:
173+
return function0(depth-1, generate)
174+
}
175+
}
176+
177+
type sinkError struct {
178+
value int
179+
}
180+
181+
func (sinkError) Error() string {
182+
return ""
183+
}
184+
185+
// Perform error formatting and consume the result to disallow optimizations against output
186+
func emulateErrorPrint(err error) {
187+
output := fmt.Sprintf("%+v", err)
188+
if len(output) > 10000 && output[1000:1004] == "DOOM" {
189+
panic("this was not supposed to happen")
190+
}
191+
}
192+
193+
// Consume error with a possible side effect to disallow optimizations against err
194+
func consumeResult(err error) {
195+
if e, ok := err.(sinkError); ok && e.value == 1 {
196+
panic("this was not supposed to happen")
197+
}
198+
}
199+
200+
// A public function to discourage optimizations against errorSink variable
201+
func ExportSink() error {
202+
return errorSink
203+
}

0 commit comments

Comments
 (0)