diff --git a/content/bench-time.pdf b/content/bench-time.pdf index 9b21db7..11fc0e6 100644 Binary files a/content/bench-time.pdf and b/content/bench-time.pdf differ diff --git a/content/cgo-handle.pdf b/content/cgo-handle.pdf index 23b8140..50b860c 100644 Binary files a/content/cgo-handle.pdf and b/content/cgo-handle.pdf differ diff --git a/content/generic-option.pdf b/content/generic-option.pdf index 68a7159..4c68ea6 100644 Binary files a/content/generic-option.pdf and b/content/generic-option.pdf differ diff --git a/content/pointer-params.pdf b/content/pointer-params.pdf index 38a164b..d98aa52 100644 Binary files a/content/pointer-params.pdf and b/content/pointer-params.pdf differ diff --git a/content/posts/bench-time.md b/content/posts/bench-time.md index 4c8715b..838a45a 100644 --- a/content/posts/bench-time.md +++ b/content/posts/bench-time.md @@ -46,7 +46,8 @@ func BenchmarkAtomic(b *testing.B) { On my target machine (CPU Quad-core Intel Core i7-7700 (-MT-MCP-) speed/max 1341/4200 MHz Kernel 5.4.0-42-generic x86_64), running this snippet with the following command: ``` -go test -run=none -bench=Atomic -benchtime=1000x -count=20 | tee b.txt && benchstat b.txt +go test -run=none -bench=Atomic -benchtime=1000x -count=20 | \ + tee b.txt && benchstat b.txt ``` The result shows: @@ -102,7 +103,8 @@ for j := 0; j < k; j++ { Thus with higher `k`, the target code grows more costly. With similar command: ``` -go test -run=none -bench=Atomic -benchtime=1000x -count=20 | tee b.txt && benchstat b.txt +go test -run=none -bench=Atomic -benchtime=1000x -count=20 | \ + tee b.txt && benchstat b.txt ``` ``` @@ -146,7 +148,8 @@ func BenchmarkWithTimer(b *testing.B) { ``` ``` -go test -v -run=none -bench=WithTimer -benchtime=100000x -count=5 -cpuprofile cpu.pprof +go test -v -run=none -bench=WithTimer -benchtime=100000x -count=5 \ + -cpuprofile cpu.pprof ``` Sadly, the graph shows a chunk of useless information where most of the costs shows as `runtime.ReadMemStats`: @@ -279,7 +282,8 @@ int main() { for (int j = 0; j < 10; j++) { std::chrono::nanoseconds since(0); for (int i = 0; i < n; i++) { - since -= std::chrono::steady_clock::now() - std::chrono::steady_clock::now(); + since -= std::chrono::steady_clock::now() - + std::chrono::steady_clock::now(); } std::cout << "avg since: " << since.count() / n << "ns \n"; } diff --git a/content/posts/cgo-handle.md b/content/posts/cgo-handle.md index 6ae18ae..bf92046 100644 --- a/content/posts/cgo-handle.md +++ b/content/posts/cgo-handle.md @@ -81,8 +81,9 @@ int myfunc(void* go_value); */ import "C" -// This funcCallback tries to avoid a runtime panic error when directly -// pass it to Cgo because it violates the pointer passing rules: +// This funcCallback tries to avoid a runtime panic error when +// directly pass it to Cgo because it violates the pointer passing +// rules: // // panic: runtime error: cgo argument has Go pointer to Go pointer var ( @@ -109,12 +110,12 @@ func main() { In above, the `gocallback` pointer on the Go side is passed through the C function `myfunc`. On the C side, there will be a call using `go_func_callback` that being called on the C, via passing the struct `gocallback` as a parameter: ```c -// myfunc will trigger a callback, c_func, whenever it is needed and pass -// the gocallback data though the void* parameter. +// myfunc will trigger a callback, c_func, whenever it is needed +// and pass the gocallback data though the void* parameter. void c_func(void *data) { void *gocallback = userData; - // the gocallback is received as a pointer, we pass it as an argument - // to the go_func_callback + // the gocallback is received as a pointer, we pass it as + // an argument to the go_func_callback go_func_callback(gocallback); } ``` @@ -158,13 +159,13 @@ type Handle uintptr // NewHandle returns a handle for a given value. // -// The handle is valid until the program calls Delete on it. The handle -// uses resources, and this package assumes that C code may hold on to -// the handle, so a program must explicitly call Delete when the handle -// is no longer needed. +// The handle is valid until the program calls Delete on it. +// The handle uses resources, and this package assumes that C +// code may hold on to the handle, so a program must explicitly +// call Delete when the handle is no longer needed. // -// The intended use is to pass the returned handle to C code, which -// passes it back to Go, which calls Value. +// The intended use is to pass the returned handle to C code, +// which passes it back to Go, which calls Value. func NewHandle(v interface{}) Handle // Value returns the associated Go value for a valid handle. @@ -172,9 +173,9 @@ func NewHandle(v interface{}) Handle // The method panics if the handle is invalid. func (h Handle) Value() interface{} -// Delete invalidates a handle. This method should only be called once -// the program no longer needs to pass the handle to C and the C code -// no longer has a copy of the handle value. +// Delete invalidates a handle. This method should only be +// called once the program no longer needs to pass the handle +// to C and the C code no longer has a copy of the handle value. // // The method panics if the handle is invalid. func (h Handle) Delete() @@ -245,7 +246,8 @@ func main() { ch := make(chan struct{}) handle := cgo.NewHandle(ch) go func() { - C.myfunc(C.uintptr_t(handle)) // myfunc will call goCallback when needed. + // myfunc will call goCallback when needed. + C.myfunc(C.uintptr_t(handle)) ... }() @@ -312,10 +314,10 @@ func NewHandle(v interface{}) Handle { k = rv.Pointer() default: - // Wrap and turn a value parameter into a pointer. This enables - // us to always store the passing object as a pointer, and helps - // to identify which of whose are initially pointers or values - // when Value is called. + // Wrap and turn a value parameter into a pointer. + // This enables us to always store the passing object + // as a pointer, and helps to identify which of whose + // are initially pointers or values when Value is called. v = &wrap{v} k = reflect.ValueOf(v).Pointer() } @@ -327,10 +329,10 @@ func NewHandle(v interface{}) Handle { Note that the implementation above treats the values differently: For `reflect.Ptr`, `reflect.UnsafePointer`, `reflect.Slice`, `reflect.Map`, `reflect.Chan`, `reflect.Func` types, they are already pointers escaped to the heap, we can safely get the address from them. For the other kinds, we need to turn them from a value to a pointer and also make sure they will always escape to the heap. That is the part: ```go - // Wrap and turn a value parameter into a pointer. This enables - // us to always store the passing object as a pointer, and helps - // to identify which of whose are initially pointers or values - // when Value is called. + // Wrap and turn a value parameter into a pointer. This + // enables us to always store the passing object as a + // pointer, and helps to identify which of whose are + // initially pointers or values when Value is called. v = &wrap{v} k = reflect.ValueOf(v).Pointer() ``` @@ -344,11 +346,12 @@ The easy case is, of course, if the address is not on the global map, then we do func NewHandle(v interface{}) Handle { ... - // v was escaped to the heap because of reflection. As Go do not have - // a moving GC (and possibly lasts true for a long future), it is - // safe to use its pointer address as the key of the global map at - // this moment. The implementation must be reconsidered if moving GC - // is introduced internally in the runtime. + // v was escaped to the heap because of reflection. As Go do + // not have a moving GC (and possibly lasts true for a long + // future), it is safe to use its pointer address as the key + // of the global map at this moment. The implementation must + // be reconsidered if moving GC is introduced internally in + // the runtime. actual, loaded := m.LoadOrStore(k, v) if !loaded { return Handle(k) @@ -374,12 +377,13 @@ func NewHandle(v interface{}) Handle { return Handle(k) } - // If the loaded pointer is inconsistent with the new pointer, - // it means the address has been used for different objects - // because of GC and its address is reused for a new Go object, - // meaning that the Handle does not call Delete explicitly when - // the old Go value is not needed. Consider this as a misuse of - // a handle, do panic. + // If the loaded pointer is inconsistent with the new + // pointer, it means the address has been used for + // different objects because of GC and its address is + // reused for a new Go object, meaning that the Handle + // does not call Delete explicitly when the old Go value + // is not needed. Consider this as a misuse of a handle, + // do panic. panic("cgo: misuse of a Handle") default: panic("cgo: Handle implementation has an internal bug") diff --git a/content/posts/generic-option.md b/content/posts/generic-option.md index 36ce9b9..3458ae6 100644 --- a/content/posts/generic-option.md +++ b/content/posts/generic-option.md @@ -79,10 +79,10 @@ func NewA(opts ...Option) *A { For example, the following four different usages both work: ```go -fmt.Printf("%#v\n", NewA()) // &main.A{v1:0, v2:0} -fmt.Printf("%#v\n", NewA(V1(42))) // &main.A{v1:42, v2:0} -fmt.Printf("%#v\n", NewA(V2(42))) // &main.A{v1:0, v2:0} -fmt.Printf("%#v\n", NewA(V1(42), V2(42))) // &main.A{v1:42, v2:0} +fmt.Printf("%#v\n", NewA()) // &A{v1:0, v2:0} +fmt.Printf("%#v\n", NewA(V1(42))) // &A{v1:42, v2:0} +fmt.Printf("%#v\n", NewA(V2(42))) // &A{v1:0, v2:0} +fmt.Printf("%#v\n", NewA(V1(42), V2(42))) // &A{v1:42, v2:0} ``` This is also super easy to deprecate an option, because we can simply let @@ -189,12 +189,12 @@ func NewB(opts ...OptionB) *B { In this way, whenever we need create a new `A` or `B`, we could: ```go -fmt.Printf("%#v\n", NewA()) // &main.A{v1:0} -fmt.Printf("%#v\n", NewA(V1ForA(42))) // &main.A{v1:42} -fmt.Printf("%#v\n", NewB()) // &main.B{v1:0, v2:0} -fmt.Printf("%#v\n", NewB(V1ForB(42))) // &main.B{v1:42, v2:0} -fmt.Printf("%#v\n", NewB(V2ForB(42))) // &main.B{v1:0, v2:42} -fmt.Printf("%#v\n", NewB(V1ForB(42), V2ForB(42))) // &main.B{v1:42, v2:42} +fmt.Printf("%#v\n", NewA()) // &A{v1:0} +fmt.Printf("%#v\n", NewA(V1ForA(42))) // &A{v1:42} +fmt.Printf("%#v\n", NewB()) // &B{v1:0, v2:0} +fmt.Printf("%#v\n", NewB(V1ForB(42))) // &B{v1:42, v2:0} +fmt.Printf("%#v\n", NewB(V2ForB(42))) // &B{v1:0, v2:42} +fmt.Printf("%#v\n", NewB(V1ForB(42), V2ForB(42))) // &B{v1:42, v2:42} ``` Although the above workaround is possible, but the actual naming and usage @@ -283,12 +283,12 @@ func NewB(opts ...Option) *B { Without further changes, one can use `V1` both for `A` and `B`, which is a quite simplification from the previous use already: ```go -fmt.Printf("%#v\n", NewA()) // &main.A{v1:0} -fmt.Printf("%#v\n", NewA(V1(42))) // &main.A{v1:42} -fmt.Printf("%#v\n", NewB()) // &main.B{v1:0, v2:0} -fmt.Printf("%#v\n", NewB(V1(42))) // &main.B{v1:42, v2:0} -fmt.Printf("%#v\n", NewB(V2(42))) // &main.B{v1:0, v2:42} -fmt.Printf("%#v\n", NewB(V1(42), V2(42))) // &main.B{v1:42, v2:42} +fmt.Printf("%#v\n", NewA()) // &A{v1:0} +fmt.Printf("%#v\n", NewA(V1(42))) // &A{v1:42} +fmt.Printf("%#v\n", NewB()) // &B{v1:0, v2:0} +fmt.Printf("%#v\n", NewB(V1(42))) // &B{v1:42, v2:0} +fmt.Printf("%#v\n", NewB(V2(42))) // &B{v1:0, v2:42} +fmt.Printf("%#v\n", NewB(V1(42), V2(42))) // &B{v1:42, v2:42} ``` However, not everything goes as expected. There is a heavy cost for this type of functional options pattern: safety. @@ -400,10 +400,16 @@ fmt.Printf("%#v\n", NewB(V1[B](42), V2[B](42))) // &main.B{v1:42, v2:42} With this design, the user of these APIs is safe because it is guaranteed by the compiler at compile-time, to disallow its misuse by the following errors: ```go -_ = NewA(V2[B](42)) // ERROR: B does not implement A -_ = NewA(V2[A](42)) // ERROR: A does not implement B -_ = NewB(V1[A](42), V2[B](42)) // ERROR: type Option[B] of V2[B](42) does not match inferred type Option[A] for Option[T] -_ = NewB(V1[B](42), V2[A](42)) // ERROR: type Option[A] of V2[A](42) does not match inferred type Option[B] for Option[T] +// ERROR: B does not implement A +_ = NewA(V2[B](42)) +// ERROR: A does not implement B +_ = NewA(V2[A](42)) +// ERROR: type Option[B] of V2[B](42) does not match +// inferred type Option[A] for Option[T] +_ = NewB(V1[A](42), V2[B](42)) +// ERROR: type Option[A] of V2[A](42) does not match +// inferred type Option[B] for Option[T] +_ = NewB(V1[B](42), V2[A](42)) ``` ## Conclusion diff --git a/content/posts/pointer-params.md b/content/posts/pointer-params.md index 196a271..c476205 100644 --- a/content/posts/pointer-params.md +++ b/content/posts/pointer-params.md @@ -80,7 +80,8 @@ func BenchmarkVec(b *testing.B) { And run as follows: ```sh -$ perflock -governor 80% go test -v -run=none -bench=. -count=10 | tee new.txt +$ perflock -governor 80% go test -v -run=none -bench=. -count=10 | \ + tee new.txt $ benchstat new.txt ``` @@ -124,7 +125,8 @@ func (v *vec) addp(u *vec) *vec { Then run the benchmark and compare the perf with the previous one: ```sh -$ perflock -governor 80% go test -v -run=none -bench=. -count=10 | tee old.txt +$ perflock -governor 80% go test -v -run=none -bench=. -count=10 | \ + tee old.txt $ benchstat old.txt new.txt name old time/op new time/op delta Vec/addv-16 4.99ns ± 1% 0.25ns ± 2% -95.05% (p=0.000 n=9+10) @@ -176,8 +178,8 @@ The dumped assumbly code is as follows: ``` "".vec.addv STEXT nosplit size=89 args=0x60 locals=0x0 funcid=0x0 0x0000 00000 (vec.go:7) TEXT "".vec.addv(SB), NOSPLIT|ABIInternal, $0-96 - 0x0000 00000 (vec.go:7) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) - 0x0000 00000 (vec.go:7) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) + 0x0000 00000 (vec.go:7) FUNCDATA $0, gclocals·...(SB) + 0x0000 00000 (vec.go:7) FUNCDATA $1, gclocals·...(SB) 0x0000 00000 (vec.go:8) MOVSD "".u+40(SP), X0 0x0006 00006 (vec.go:8) MOVSD "".v+8(SP), X1 0x000c 00012 (vec.go:8) ADDSD X1, X0 @@ -197,8 +199,8 @@ The dumped assumbly code is as follows: 0x0058 00088 (vec.go:8) RET "".(*vec).addp STEXT nosplit size=73 args=0x18 locals=0x0 funcid=0x0 0x0000 00000 (vec.go:11) TEXT "".(*vec).addp(SB), NOSPLIT|ABIInternal, $0-24 - 0x0000 00000 (vec.go:11) FUNCDATA $0, gclocals·522734ad228da40e2256ba19cf2bc72c(SB) - 0x0000 00000 (vec.go:11) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB) + 0x0000 00000 (vec.go:11) FUNCDATA $0, gclocals·...(SB) + 0x0000 00000 (vec.go:11) FUNCDATA $1, gclocals·...(SB) 0x0000 00000 (vec.go:12) MOVQ "".u+16(SP), AX 0x0005 00005 (vec.go:12) MOVSD (AX), X0 0x0009 00009 (vec.go:12) MOVQ "".v+8(SP), CX @@ -329,7 +331,8 @@ func main() { Name: fmt.Sprintf("s%d", i), Properties: strings.Join(ps, "\n"), Addv: strings.Join(adv, "\n"), - Addp: strings.Join(adpl, ",") + " = " + strings.Join(adpr, ","), + Addp: strings.Join(adpl, ",") + " = " + + strings.Join(adpr, ","), }) if err != nil { panic(err) @@ -363,7 +366,8 @@ v2 := s%d{%s}`, i, numstr1, i, numstr2), if err != nil { panic(err) } - if err := ioutil.WriteFile("impl_test.go", out, 0660); err != nil { + err = ioutil.WriteFile("impl_test.go", out, 0660) + if err != nil { panic(err) } } @@ -373,7 +377,8 @@ If we generate our test code and perform the same benchmark procedure again: ```bash $ go generate -$ perflock -governor 80% go test -v -run=none -bench=. -count=10 | tee inline.txt +$ perflock -governor 80% go test -v -run=none -bench=. -count=10 | \ + tee inline.txt $ benchstat inline.txt name time/op Vec/addv-s0-16 0.25ns ± 0% diff --git a/content/posts/ultimate-channel.md b/content/posts/ultimate-channel.md index 3476e1a..db31bd7 100644 --- a/content/posts/ultimate-channel.md +++ b/content/posts/ultimate-channel.md @@ -90,16 +90,17 @@ func (p *RenderProfile) Draw() interface{} { func main() { // draw is a channel for receiving finished draw calls. draw := make(chan interface{}) - // change is a channel to receive notification of the change of rendering settings. + // change is a channel to receive notification of the change + // of rendering settings. change := make(chan ResizeEvent) // Rendering Thread // - // Sending draw calls to the event thread in order to draw pictures. - // The thread sends darw calls to the draw channel, using the same - // rendering setting id. If there is a change of rendering setting, - // the event thread notifies the rendering setting change, and here - // increases the rendering setting id. + // Sending draw calls to the event thread in order to draw + // pictures. The thread sends darw calls to the draw channel, + // using the same rendering setting id. If there is a change + // of rendering setting, the event thread notifies the rendering + // setting change, and here increases the rendering setting id. go func() { p := &RenderProfile{id: 0, width: 800, height: 500} for { @@ -117,17 +118,17 @@ func main() { // Event Thread // - // Process events every 100 ms. Otherwise, process drawcall request - // upon-avaliable. + // Process events every 100 ms. Otherwise, process drawcall + // request upon-avaliable. event := time.NewTicker(100 * time.Millisecond) for { select { case id := <-draw: println(id) case <-event.C: - // Notify the rendering thread there is a change regarding - // rendering settings. We simulate a random size at every - // event processing loop. + // Notify the rendering thread there is a change + // regarding rendering settings. We simulate a + // random size at every event processing loop. change <- ResizeEvent{ width: int(rand.Float64() * 100), height: int(rand.Float64() * 100), @@ -249,7 +250,8 @@ constructed: // MakeChan returns a sender and a receiver of a buffered channel // with infinite capacity. // -// Warning: this implementation can be easily misuse, see discussion below +// Warning: this implementation can be easily misuse, +// see discussion below func MakeChan() (chan<- interface{}, <-chan interface{}) { in, out := make(chan interface{}), make(chan interface{}) @@ -369,11 +371,13 @@ to construct a type-safe, arbitrary sized channel: // // If the given size is positive, the returned channel is a regular // fix-sized buffered channel. -// If the given size is zero, the returned channel is an unbuffered channel. -// If the given size is -1, the returned an unbounded channel contains an -// internal buffer with infinite capacity. +// If the given size is zero, the returned channel is an unbuffered +// channel. +// If the given size is -1, the returned an unbounded channel +// contains an internal buffer with infinite capacity. // -// Warning: this implementation can be easily misuse, see discussion below +// Warning: this implementation can be easily misuse, +// see discussion below func MakeChan[T any](size int) (chan<- T, <-chan T) { switch { case size == 0: @@ -433,7 +437,7 @@ func main() { } ``` -*_This code is executable on go2go playground:_ https://go2goplay.golang.org/p/krLWm7ZInnL +*_This code is executable on go2go playground:_ https://go.dev/play/p/krLWm7ZInnL ## Design Concerns and Real-world Use Cases diff --git a/content/posts/zero-alloc-call-sched.md b/content/posts/zero-alloc-call-sched.md index 9a574b6..acf319d 100644 --- a/content/posts/zero-alloc-call-sched.md +++ b/content/posts/zero-alloc-call-sched.md @@ -71,7 +71,8 @@ Luckily, there is a method called `LockOSThread` offered from the `runtime` package, provides the same feature we want: ```go -// LockOSThread wires the calling goroutine to its current operating system thread. +// LockOSThread wires the calling goroutine to its current operating +// system thread. // The calling goroutine will always execute in that thread, // and no other goroutine will execute in it, // until the calling goroutine has made as many calls to @@ -79,9 +80,9 @@ Luckily, there is a method called `LockOSThread` offered from the // If the calling goroutine exits without unlocking the thread, // the thread will be terminated. // -// All init functions are run on the startup thread. Calling LockOSThread -// from an init function will cause the main function to be invoked on -// that thread. +// All init functions are run on the startup thread. Calling +// LockOSThread from an init function will cause the main function +// to be invoked on that thread. // // A goroutine should call LockOSThread before calling OS services or // non-Go library functions that depend on per-thread state. @@ -97,7 +98,8 @@ allows us to identify, at least, the main thread. When we would like to wrapping thread scheduling as a package `mainthread`, we can do something like the following:: -```go {linenos=inline,hl_lines=[13,16],linenostart=1} + +```go package mainthread // import "x/mainthread" import "runtime" @@ -118,7 +120,8 @@ func Call(f func()) As a user of such a package, one can: -```go {linenos=inline,hl_lines=[15],linenostart=1} + +```go package main func main() { @@ -131,7 +134,8 @@ func fn() { func gn() { - // Wherever gn is running, the call will be executed on the main thread. + // Wherever gn is running, the call will be executed on + // the main thread. mainthread.Call(func() { // ... do whatever we want to run on the main thread ... }) @@ -262,7 +266,8 @@ func NewWindow() (*Win, error) { err error ) mainthread.Call(func() { - w.win, err = glfw.CreateWindow(640, 480, "golang.design/research", nil, nil) + w.win, err = glfw.CreateWindow(640, 480, + "golang.design/research", nil, nil) if err != nil { return } @@ -437,7 +442,8 @@ The first optimization comes to the attempt to avoid allocating channels. In our `Call` implementation, we allocate a signal channel for every function that we need to call from the main thread: -```go {linenos=inline,hl_lines=[3],linenostart=1} + +```go // Call calls f on the main thread and blocks until f finishes. func Call(f func()) { done := make(chan struct{}) // allocation! @@ -475,13 +481,15 @@ type hchan struct { A well-known trick to avoid repetitive allocation is to use the `sync.Pool`. One can: -```go {linenos=inline,hl_lines=["1-3", 6, 7],linenostart=1} + +```go var donePool = sync.Pool{New: func() interface{} { return make(chan struct{}) }} func Call(f func()) { - done := donePool.Get().(chan struct{}) // reuse signal channel via sync.Pool! + // reuse signal channel via sync.Pool! + done := donePool.Get().(chan struct{}) defer donePool.Put(done) funcQ <- func() { @@ -495,7 +503,8 @@ func Call(f func()) { With that simple optimization, a benchmarked result indicates an 80% reduction of memory usage: -```txt {linenos=inline,hl_lines=[3,7,11],linenostart=1} + +```txt name old time/op new time/op delta DirectCall-8 0.95ns ±1% 0.95ns ±1% ~ (p=0.631 n=10+10) MainThreadCall-8 448ns ±0% 440ns ±0% -1.83% (p=0.000 n=9+9) @@ -546,7 +555,8 @@ cause an escape by design. To avoid the escaping function literal, instead of using a function wrapper, we can send a struct: -```go {linenos=inline,hl_lines=["1-4", 10],linenostart=1} + +```go type funcdata struct { fn func() done chan struct{} @@ -563,7 +573,8 @@ func Call(f func()) { and when we receive the `funcdata`: -```go {linenos=inline,hl_lines=["6-8"],linenostart=1} + +```go func Init(main func()) { ... @@ -582,7 +593,8 @@ func Init(main func()) { After such an optimization, a re-benchmarked result indicates that we hint the zero-allocation goal: -```txt {linenos=table,hl_lines=[3,7,11],linenostart=1} + +```txt name old time/op new time/op delta DirectCall-8 0.95ns ±1% 0.95ns ±1% ~ (p=0.896 n=10+10) MainThreadCall-8 448ns ±0% 366ns ±1% -18.17% (p=0.000 n=9+9) @@ -623,7 +635,8 @@ operation to a runtime function `runtime.newobject`. One can add 3 more lines and prints, which is exactly calling this function using `runtime.FuncForPC`: -```go {linenos=inline,hl_lines=["3-5"],linenostart=1} + +```go // src/runtime/malloc.go func newobject(typ *_type) unsafe.Pointer { f := FuncForPC(getcallerpc()) // add this @@ -646,7 +659,8 @@ similar to below: It demonstrates how and why the allocation still happens: -```go {linenos=inline,hl_lines=[23],linenostart=1} + +```go // ch <- elem func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... diff --git a/content/ultimate-channel.pdf b/content/ultimate-channel.pdf index 4db722b..9371c5c 100644 Binary files a/content/ultimate-channel.pdf and b/content/ultimate-channel.pdf differ diff --git a/content/zero-alloc-call-sched.pdf b/content/zero-alloc-call-sched.pdf index 63267d5..8f8a030 100644 Binary files a/content/zero-alloc-call-sched.pdf and b/content/zero-alloc-call-sched.pdf differ diff --git a/pdfgen.go b/pdfgen.go index 4b20e87..2e6de80 100644 --- a/pdfgen.go +++ b/pdfgen.go @@ -40,7 +40,7 @@ func init() { func usage() { fmt.Fprintf(os.Stderr, `pdfgen converts a golang.design research markdown file to a pdf. -usage: pdfgen content/posts/bench-time.md +usage: pdfgen bench-time.md `) } @@ -104,14 +104,14 @@ func main() { // Prepare all content. - ref := "content/posts/ref.tex" + ref := "ref.tex" references := parseReferences(b) if err := os.WriteFile(ref, []byte(references), os.ModePerm); err != nil { log.Fatalf("pdfgen: cannot create reference file: %v", err) } defer os.Remove(ref) - article := "content/posts/article.md" + article := "article.md" if err := os.WriteFile(article, []byte(content), os.ModePerm); err != nil { log.Fatalf("pdfgen: cannot create temporary file: %v", err) } @@ -119,8 +119,7 @@ func main() { // Generate pdf - before, after, _ := strings.Cut(strings.TrimSuffix(path, ".md")+".pdf", "/posts") - dst := before + after + dst := "../" + strings.TrimSuffix(path, ".md") + ".pdf" cmd := exec.Command("pandoc", article, ref, "-V", "linkcolor:blue", "--pdf-engine=xelatex",