From 5d43367082723b24bd2101103a3ff0d51c208b7a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 25 Oct 2022 13:08:33 +0300 Subject: [PATCH 1/2] emit: optimize Bool GAS cost NOT is 1 byte shorter and 2048 times cheaper than CONVERT. Inspired by neo-project/neo-vm#493. --- cli/nep_test/nep17_test.go | 2 +- cli/testdata/wallet1_solo.json | 3 +- cli/vm/cli_test.go | 4 +- pkg/services/rpcsrv/server_test.go | 4 +- pkg/smartcontract/param_type_test.go | 2 +- pkg/vm/emit/emit.go | 9 ++-- pkg/vm/emit/emit_test.go | 65 +++++++++++++--------------- 7 files changed, 42 insertions(+), 47 deletions(-) diff --git a/cli/nep_test/nep17_test.go b/cli/nep_test/nep17_test.go index 081780928..6aeed8df7 100644 --- a/cli/nep_test/nep17_test.go +++ b/cli/nep_test/nep17_test.go @@ -99,7 +99,7 @@ func TestNEP17Balance(t *testing.T) { } e.CheckNextLine(t, "^\\s*$") - addr4, err := address.StringToUint160("NQ3nAdFQXzemHC9uvr4af2Ysap6aZJpqgN") // deployed verify.go contract + addr4, err := address.StringToUint160("NfWu6j9KPLQMsWLfHz9iZRy5sNw2bUZWQL") // deployed verify.go contract require.NoError(t, err) e.CheckNextLine(t, "^Account "+address.Uint160ToString(addr4)) e.CheckEOF(t) diff --git a/cli/testdata/wallet1_solo.json b/cli/testdata/wallet1_solo.json index d71a06198..4c9345ae1 100644 --- a/cli/testdata/wallet1_solo.json +++ b/cli/testdata/wallet1_solo.json @@ -61,11 +61,10 @@ "isDefault": false }, { - "address": "NQ3nAdFQXzemHC9uvr4af2Ysap6aZJpqgN", + "address": "NfWu6j9KPLQMsWLfHz9iZRy5sNw2bUZWQL", "key": "6PYSATFztBa3CHjSR6sLAKungUEAbQUCVE16KzmaQQ38gLeYGZ9Knd5mGv", "label": "verify", "contract": { - "script": "EdsgQFcAA0BXAQR4eXp7VBTAcAwOT25ORVAxMVBheW1lbnRoUEGVAW9hIUA=", "parameters": [], "deployed": true }, diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index 4552f9253..724e2d118 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -914,7 +914,7 @@ func TestRunWithHistoricState(t *testing.T) { e.checkNextLine(t, "READY: loaded 36 instructions") e.checkStack(t, []byte{1}) e.checkNextLine(t, "READY: loaded 36 instructions") - e.checkNextLineExact(t, "Error: at instruction 31 (SYSCALL): failed to invoke syscall 1381727586: called contract a00e3c2643a08a452d8b0bdd31849ae11a17c445 not found: key not found\n") + e.checkNextLineExact(t, "Error: at instruction 31 (SYSCALL): failed to invoke syscall 1381727586: called contract cd583ac7a1a4faef70d6e9f513bc988dde22f672 not found: key not found\n") } func TestEvents(t *testing.T) { @@ -939,7 +939,7 @@ func TestEvents(t *testing.T) { }), }), } - e.checkNextLine(t, "READY: loaded 44 instructions") + e.checkNextLine(t, "READY: loaded 43 instructions") e.checkStack(t, stackitem.Null{}) e.checkEvents(t, true, expectedEvent) // automatically printed after `run` command e.checkEvents(t, false, expectedEvent) // printed after `events` command diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index 93f015121..d61629d51 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -2557,7 +2557,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] t.Run("contract-based verification with parameters", func(t *testing.T) { verAcc, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash) require.NoError(t, err) - checkContract(t, verAcc, []byte{}, 737530) // No C# match, but we believe it's OK and it differs from the one above. + checkContract(t, verAcc, []byte{}, 490890) // No C# match, but we believe it's OK and it differs from the one above. }) t.Run("contract-based verification with invocation script", func(t *testing.T) { verAcc, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash) @@ -2567,7 +2567,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] emit.Int(invocWriter.BinWriter, 5) emit.String(invocWriter.BinWriter, "") invocScript := invocWriter.Bytes() - checkContract(t, verAcc, invocScript, 640360) // No C# match, but we believe it's OK and it has a specific invocation script overriding anything server-side. + checkContract(t, verAcc, invocScript, 393720) // No C# match, but we believe it's OK and it has a specific invocation script overriding anything server-side. }) }) } diff --git a/pkg/smartcontract/param_type_test.go b/pkg/smartcontract/param_type_test.go index af643b042..83294a316 100644 --- a/pkg/smartcontract/param_type_test.go +++ b/pkg/smartcontract/param_type_test.go @@ -347,7 +347,7 @@ func TestEncodeDefaultValue(t *testing.T) { for p, l := range map[ParamType]int{ UnknownType: 0, AnyType: 66, - BoolType: 3, + BoolType: 2, IntegerType: 33, ByteArrayType: 66, StringType: 66, diff --git a/pkg/vm/emit/emit.go b/pkg/vm/emit/emit.go index 6ce7fa0ac..fedf8929d 100644 --- a/pkg/vm/emit/emit.go +++ b/pkg/vm/emit/emit.go @@ -31,12 +31,11 @@ func Opcodes(w *io.BinWriter, ops ...opcode.Opcode) { // Bool emits a bool type to the given buffer. func Bool(w *io.BinWriter, ok bool) { - if ok { - Opcodes(w, opcode.PUSHT) - } else { - Opcodes(w, opcode.PUSHF) + var opVal = opcode.PUSHF + if !ok { + opVal = opcode.PUSHT } - Instruction(w, opcode.CONVERT, []byte{byte(stackitem.BooleanT)}) + Opcodes(w, opVal, opcode.NOT) } func padRight(s int, buf []byte) []byte { diff --git a/pkg/vm/emit/emit_test.go b/pkg/vm/emit/emit_test.go index f2f135725..130cc5e54 100644 --- a/pkg/vm/emit/emit_test.go +++ b/pkg/vm/emit/emit_test.go @@ -231,35 +231,34 @@ func TestEmitArray(t *testing.T) { assert.EqualValues(t, opcode.PUSHDATA1, res[0]) assert.EqualValues(t, 2, res[1]) assert.EqualValues(t, []byte{0xCA, 0xFE}, res[2:4]) - assert.EqualValues(t, opcode.PUSHT, res[4]) - assert.EqualValues(t, opcode.CONVERT, res[5]) - assert.EqualValues(t, stackitem.BooleanT, res[6]) - assert.EqualValues(t, opcode.PUSHDATA1, res[7]) - assert.EqualValues(t, 3, res[8]) - assert.EqualValues(t, []byte("str"), res[9:12]) - assert.EqualValues(t, opcode.PUSH1, res[12]) - assert.EqualValues(t, opcode.PUSHNULL, res[13]) - assert.EqualValues(t, opcode.PUSH2, res[14]) - assert.EqualValues(t, opcode.PUSH1, res[15]) - assert.EqualValues(t, opcode.PUSH2, res[16]) - assert.EqualValues(t, opcode.PACK, res[17]) - assert.EqualValues(t, opcode.PUSHINT128, res[18]) - assert.EqualValues(t, veryBig, bigint.FromBytes(res[19:35])) - assert.EqualValues(t, opcode.PUSH0, res[35]) - assert.EqualValues(t, opcode.PUSHDATA1, res[36]) - assert.EqualValues(t, 32, res[37]) - assert.EqualValues(t, u256.BytesBE(), res[38:70]) - assert.EqualValues(t, opcode.PUSHDATA1, res[70]) - assert.EqualValues(t, 20, res[71]) - assert.EqualValues(t, u160.BytesBE(), res[72:92]) - assert.EqualValues(t, opcode.PUSHDATA1, res[92]) - assert.EqualValues(t, 32, res[93]) - assert.EqualValues(t, u256.BytesBE(), res[94:126]) - assert.EqualValues(t, opcode.PUSHDATA1, res[126]) - assert.EqualValues(t, 20, res[127]) - assert.EqualValues(t, u160.BytesBE(), res[128:148]) + assert.EqualValues(t, opcode.PUSHF, res[4]) + assert.EqualValues(t, opcode.NOT, res[5]) + assert.EqualValues(t, opcode.PUSHDATA1, res[6]) + assert.EqualValues(t, 3, res[7]) + assert.EqualValues(t, []byte("str"), res[8:11]) + assert.EqualValues(t, opcode.PUSH1, res[11]) + assert.EqualValues(t, opcode.PUSHNULL, res[12]) + assert.EqualValues(t, opcode.PUSH2, res[13]) + assert.EqualValues(t, opcode.PUSH1, res[14]) + assert.EqualValues(t, opcode.PUSH2, res[15]) + assert.EqualValues(t, opcode.PACK, res[16]) + assert.EqualValues(t, opcode.PUSHINT128, res[17]) + assert.EqualValues(t, veryBig, bigint.FromBytes(res[18:34])) + assert.EqualValues(t, opcode.PUSH0, res[34]) + assert.EqualValues(t, opcode.PUSHDATA1, res[35]) + assert.EqualValues(t, 32, res[36]) + assert.EqualValues(t, u256.BytesBE(), res[37:69]) + assert.EqualValues(t, opcode.PUSHDATA1, res[69]) + assert.EqualValues(t, 20, res[70]) + assert.EqualValues(t, u160.BytesBE(), res[71:91]) + assert.EqualValues(t, opcode.PUSHDATA1, res[91]) + assert.EqualValues(t, 32, res[92]) + assert.EqualValues(t, u256.BytesBE(), res[93:125]) + assert.EqualValues(t, opcode.PUSHDATA1, res[125]) + assert.EqualValues(t, 20, res[126]) + assert.EqualValues(t, u160.BytesBE(), res[127:147]) + assert.EqualValues(t, opcode.PUSHNULL, res[147]) assert.EqualValues(t, opcode.PUSHNULL, res[148]) - assert.EqualValues(t, opcode.PUSHNULL, res[149]) }) t.Run("empty", func(t *testing.T) { @@ -281,12 +280,10 @@ func TestEmitBool(t *testing.T) { Bool(buf.BinWriter, true) Bool(buf.BinWriter, false) result := buf.Bytes() - assert.EqualValues(t, opcode.PUSH1, result[0]) - assert.EqualValues(t, opcode.CONVERT, result[1]) - assert.EqualValues(t, stackitem.BooleanT, result[2]) - assert.EqualValues(t, opcode.PUSH0, result[3]) - assert.EqualValues(t, opcode.CONVERT, result[4]) - assert.EqualValues(t, stackitem.BooleanT, result[5]) + assert.EqualValues(t, opcode.PUSH0, result[0]) + assert.EqualValues(t, opcode.NOT, result[1]) + assert.EqualValues(t, opcode.PUSH1, result[2]) + assert.EqualValues(t, opcode.NOT, result[3]) } func TestEmitOpcode(t *testing.T) { From 4e58bd74116499165614dc7561af43e9c2a2e169 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 25 Oct 2022 18:20:55 +0300 Subject: [PATCH 2/2] compiler: use shorter and cheaper sequence to convert to Boolean --- pkg/compiler/codegen.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 871433851..f21d421c4 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1945,10 +1945,16 @@ func transformArgs(fs *funcScope, fun ast.Expr, isBuiltin bool, args []ast.Expr) // emitConvert converts the top stack item to the specified type. func (c *codegen) emitConvert(typ stackitem.Type) { - emit.Opcodes(c.prog.BinWriter, opcode.DUP) - emit.Instruction(c.prog.BinWriter, opcode.ISTYPE, []byte{byte(typ)}) - emit.Instruction(c.prog.BinWriter, opcode.JMPIF, []byte{2 + 2}) // After CONVERT. - emit.Instruction(c.prog.BinWriter, opcode.CONVERT, []byte{byte(typ)}) + if typ == stackitem.BooleanT { + // DUP + ISTYPE + JMPIF costs 3 already with CONVERT of a cost 8192. + // NOT+NOT at the same time costs 4 and always works (and is shorter). + emit.Opcodes(c.prog.BinWriter, opcode.NOT, opcode.NOT) + } else { + emit.Opcodes(c.prog.BinWriter, opcode.DUP) + emit.Instruction(c.prog.BinWriter, opcode.ISTYPE, []byte{byte(typ)}) + emit.Instruction(c.prog.BinWriter, opcode.JMPIF, []byte{2 + 2}) // After CONVERT. + emit.Instruction(c.prog.BinWriter, opcode.CONVERT, []byte{byte(typ)}) + } } func (c *codegen) convertByteArray(elems []ast.Expr) {