Skip to content

Commit 171157a

Browse files
odeke-emthehowl
andauthored
perf(gnolang): improve print and println performance (gnolang#3952)
This change is the result of seeing print/println being reported in bugs as needing gas metering but on examination, noticed that the code causes a RAM and CPU bloat, so this makes an improvement in all dimensions by invoking: * io.WriteString(m.Output, segments) instead of m.Output.Write([]byte(segments)) * in cases where `!debug`, we don't need to construct the concatenated string delimited by " " and can instead write the Sprint-ed string segments as we encounter them ```shell $ benchstat before_println.txt after_println3.txt name old time/op new time/op delta GnoPrintln-8 1.94ms ± 2% 1.86ms ± 4% -4.02% (p=0.001 n=9+9) name old alloc/op new alloc/op delta GnoPrintln-8 1.92MB ± 0% 1.81MB ± 0% -5.92% (p=0.000 n=10+10) name old allocs/op new allocs/op delta GnoPrintln-8 19.0k ± 0% 16.0k ± 0% -15.76% (p=0.002 n=8+10) ``` Fixes gnolang#3951 --------- Co-authored-by: Morgan Bazalgette <[email protected]>
1 parent 6a70957 commit 171157a

File tree

6 files changed

+184
-38
lines changed

6 files changed

+184
-38
lines changed

gnovm/pkg/gnolang/machine_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/gnolang/gno/tm2/pkg/store/dbadapter"
1010
"github.com/gnolang/gno/tm2/pkg/store/iavl"
1111
stypes "github.com/gnolang/gno/tm2/pkg/store/types"
12-
"github.com/google/go-cmp/cmp"
1312
"github.com/stretchr/testify/assert"
1413
)
1514

@@ -109,9 +108,7 @@ func TestMachineString(t *testing.T) {
109108
for _, tt := range cases {
110109
t.Run(tt.name, func(t *testing.T) {
111110
got := tt.in.String()
112-
if diff := cmp.Diff(got, tt.want); diff != "" {
113-
t.Fatalf("Mismatch: got - want +\n%s", diff)
114-
}
111+
assert.Equal(t, tt.want, got)
115112
})
116113
}
117114
}

gnovm/pkg/gnolang/uverse.go

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package gnolang
22

33
import (
4+
"bytes"
45
"fmt"
5-
"strings"
6+
"io"
67

78
bm "github.com/gnolang/gno/gnovm/pkg/benchops"
89
)
@@ -708,6 +709,7 @@ func makeUverseNode() {
708709
return
709710
},
710711
)
712+
711713
// NOTE: panic is its own statement type, and is not defined as a function.
712714
defNative("print",
713715
Flds( // params
@@ -716,18 +718,7 @@ func makeUverseNode() {
716718
nil, // results
717719
func(m *Machine) {
718720
arg0 := m.LastBlock().GetParams1()
719-
xv := arg0
720-
xvl := xv.TV.GetLength()
721-
ss := make([]string, xvl)
722-
for i := 0; i < xvl; i++ {
723-
ev := xv.TV.GetPointerAtIndexInt(m.Store, i).Deref()
724-
ss[i] = ev.Sprint(m)
725-
}
726-
rs := strings.Join(ss, " ")
727-
if debug {
728-
print(rs)
729-
}
730-
m.Output.Write([]byte(rs))
721+
uversePrint(m, arg0, false)
731722
},
732723
)
733724
defNative("println",
@@ -737,18 +728,7 @@ func makeUverseNode() {
737728
nil, // results
738729
func(m *Machine) {
739730
arg0 := m.LastBlock().GetParams1()
740-
xv := arg0
741-
xvl := xv.TV.GetLength()
742-
ss := make([]string, xvl)
743-
for i := 0; i < xvl; i++ {
744-
ev := xv.TV.GetPointerAtIndexInt(m.Store, i).Deref()
745-
ss[i] = ev.Sprint(m)
746-
}
747-
rs := strings.Join(ss, " ") + "\n"
748-
if debug {
749-
println("DEBUG/stdout: " + rs)
750-
}
751-
m.Output.Write([]byte(rs))
731+
uversePrint(m, arg0, true)
752732
},
753733
)
754734
defNative("recover",
@@ -786,3 +766,38 @@ func copyListToRunes(dst []rune, tvs []TypedValue) {
786766
dst[i] = tvs[i].GetInt32()
787767
}
788768
}
769+
770+
// uversePrint is used for the print and println functions.
771+
// println passes newline = true.
772+
// xv contains the variadic argument passed to the function.
773+
func uversePrint(m *Machine, xv PointerValue, newline bool) {
774+
xvl := xv.TV.GetLength()
775+
switch xvl {
776+
case 0:
777+
if newline {
778+
m.Output.Write(bNewline)
779+
}
780+
case 1:
781+
ev := xv.TV.GetPointerAtIndexInt(m.Store, 0).Deref()
782+
res := ev.Sprint(m)
783+
io.WriteString(m.Output, res)
784+
if newline {
785+
m.Output.Write(bNewline)
786+
}
787+
default:
788+
var buf bytes.Buffer
789+
for i := 0; i < xvl; i++ {
790+
if i != 0 { // Not the last item.
791+
buf.WriteByte(' ')
792+
}
793+
ev := xv.TV.GetPointerAtIndexInt(m.Store, i).Deref()
794+
buf.WriteString(ev.Sprint(m))
795+
}
796+
if newline {
797+
buf.WriteByte('\n')
798+
}
799+
m.Output.Write(buf.Bytes())
800+
}
801+
}
802+
803+
var bNewline = []byte("\n")

gnovm/pkg/gnolang/uverse_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
package gnolang
22

33
import (
4+
"bytes"
45
"testing"
6+
7+
"github.com/gnolang/gno/gnovm"
8+
"github.com/gnolang/gno/tm2/pkg/db/memdb"
9+
"github.com/gnolang/gno/tm2/pkg/store/dbadapter"
10+
"github.com/gnolang/gno/tm2/pkg/store/iavl"
11+
stypes "github.com/gnolang/gno/tm2/pkg/store/types"
12+
"github.com/stretchr/testify/assert"
513
)
614

715
type uverseTestCases struct {
@@ -158,3 +166,135 @@ func TestIssue1337PrintNilSliceAsUndefined(t *testing.T) {
158166
})
159167
}
160168
}
169+
170+
var pSink any = nil
171+
172+
func BenchmarkGnoPrintln(b *testing.B) {
173+
var buf bytes.Buffer
174+
db := memdb.NewMemDB()
175+
baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{})
176+
iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{})
177+
store := NewStore(nil, baseStore, iavlStore)
178+
179+
m := NewMachineWithOptions(MachineOptions{
180+
Output: &buf,
181+
Store: store,
182+
})
183+
184+
program := `package p
185+
func main() {
186+
for i := 0; i < 1000; i++ {
187+
println("abcdeffffffffffffffff1222 11111 11111")
188+
}
189+
}`
190+
m.RunMemPackage(&gnovm.MemPackage{
191+
Name: "p",
192+
Path: "p",
193+
Files: []*gnovm.MemFile{
194+
{Name: "a.gno", Body: program},
195+
},
196+
}, false)
197+
198+
b.ReportAllocs()
199+
b.ResetTimer()
200+
201+
for i := 0; i < b.N; i++ {
202+
buf.Reset()
203+
m.RunStatement(S(Call(Nx("main"))))
204+
pSink = buf.String()
205+
}
206+
207+
if pSink == nil {
208+
b.Fatal("Benchmark did not run!")
209+
}
210+
pSink = nil
211+
}
212+
213+
func TestGnoPrintAndPrintln(t *testing.T) {
214+
tests := []struct {
215+
name string
216+
srcArgs string
217+
want string
218+
}{
219+
{
220+
"print with no args",
221+
"print()",
222+
"",
223+
},
224+
{
225+
"print with 1 arg",
226+
`print("1")`,
227+
"1",
228+
},
229+
{
230+
"print with 2 args",
231+
`print("1", 2)`,
232+
"1 2",
233+
},
234+
{
235+
"print with 3 args",
236+
`print("1", 2, "*")`,
237+
"1 2 *",
238+
},
239+
{
240+
"print with own spaces",
241+
`print("1 ", 2, "*")`,
242+
"1 2 *",
243+
},
244+
{
245+
"println with no args",
246+
"println()",
247+
"\n",
248+
},
249+
{
250+
"print with 1 arg",
251+
`println("1")`,
252+
"1\n",
253+
},
254+
{
255+
"println with 2 args",
256+
`println("1", 2)`,
257+
"1 2\n",
258+
},
259+
{
260+
"println with 3 args",
261+
`println("1", 2, "*")`,
262+
"1 2 *\n",
263+
},
264+
{
265+
"println with own spaces",
266+
`println("1 ", 2, "*")`,
267+
"1 2 *\n",
268+
},
269+
}
270+
271+
for _, tt := range tests {
272+
t.Run(tt.name, func(t *testing.T) {
273+
var buf bytes.Buffer
274+
db := memdb.NewMemDB()
275+
baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{})
276+
iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{})
277+
store := NewStore(nil, baseStore, iavlStore)
278+
279+
m := NewMachineWithOptions(MachineOptions{
280+
Output: &buf,
281+
Store: store,
282+
})
283+
284+
program := `package p
285+
func main() {` + tt.srcArgs + "\n}"
286+
m.RunMemPackage(&gnovm.MemPackage{
287+
Name: "p",
288+
Path: "p",
289+
Files: []*gnovm.MemFile{
290+
{Name: "a.gno", Body: program},
291+
},
292+
}, false)
293+
294+
buf.Reset()
295+
m.RunStatement(S(Call(Nx("main"))))
296+
got := buf.String()
297+
assert.Equal(t, tt.want, got)
298+
})
299+
}
300+
}

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ require (
1414
github.com/davecgh/go-spew v1.1.1
1515
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
1616
github.com/fortytw2/leaktest v1.3.0
17-
github.com/google/go-cmp v0.6.0
1817
github.com/google/gofuzz v1.2.0
1918
github.com/gorilla/websocket v1.5.3
2019
github.com/libp2p/go-buffer-pool v0.1.0

tm2/pkg/bft/rpc/lib/server/write_endpoints_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"net/http/httptest"
77
"testing"
88

9-
"github.com/google/go-cmp/cmp"
109
"github.com/stretchr/testify/assert"
1110

1211
types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types"
@@ -27,7 +26,5 @@ func TestWriteListOfEndpoints(t *testing.T) {
2726
assert.NoError(t, err)
2827
gotResp := string(blob)
2928
wantResp := `<html><body><br>Available endpoints:<br><a href="//localhost/d">//localhost/d</a></br><br>Endpoints that require arguments:<br><a href="//localhost/c?s=_&i=_">//localhost/c?s=_&i=_</a></br></body></html>`
30-
if diff := cmp.Diff(gotResp, wantResp); diff != "" {
31-
t.Fatalf("Mismatch response: got - want +\n%s", diff)
32-
}
29+
assert.Equal(t, wantResp, gotResp)
3330
}

tm2/pkg/sdk/auth/params_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"reflect"
55
"testing"
66

7-
"github.com/google/go-cmp/cmp"
7+
"github.com/stretchr/testify/assert"
88
"github.com/stretchr/testify/require"
99
)
1010

@@ -123,9 +123,7 @@ func TestParamsString(t *testing.T) {
123123
for _, tt := range cases {
124124
t.Run(tt.name, func(t *testing.T) {
125125
got := tt.params.String()
126-
if diff := cmp.Diff(got, tt.want); diff != "" {
127-
t.Fatalf("Mismatch: got - want +\n%s", diff)
128-
}
126+
assert.Equal(t, tt.want, got)
129127
})
130128
}
131129
}

0 commit comments

Comments
 (0)