Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve gc for isunspendable method #1615

Merged
merged 1 commit into from
Feb 2, 2021

Conversation

Rjected
Copy link
Collaborator

@Rjected Rjected commented Aug 15, 2020

Through profiling, I've found that the IsUnspendable method is a large source of allocations during the sync process.

In an effort to reduce the allocations made by this method, and in the sync process overall, I've simply removed the part of parseScriptTemplate which allocates and appends to an array (which never ends up being used by IsUnspendable) and pointed IsUnspendable to that method, called checkScriptTemplateParseable.

To show that this actually did reduce the allocations (and runtime) of the IsUnspendable method, I've created a benchmark and run it on both versions of the code, once on the e761244 commit, and once on the 0963732 commit.
I used the command on the older commit:

go test -v -bench=BenchmarkIsUnspendable -run=^\$ -benchmem -benchtime 10s -count 30 >> ~/unspendableold.txt

I then used this command on the newer commit:

go test -v -bench=BenchmarkIsUnspendable -run=^\$ -benchmem -benchtime 10s -count 30 >> ~/unspendablenew.txt

After running these, benchstat yielded the following result:

name             old time/op    new time/op    delta
IsUnspendable-8     283ns ± 3%      47ns ± 2%  -83.34%  (p=0.000 n=28+27)

name             old alloc/op   new alloc/op   delta
IsUnspendable-8      768B ± 0%       32B ± 0%  -95.83%  (p=0.000 n=30+30)

name             old allocs/op  new allocs/op  delta
IsUnspendable-8      1.00 ± 0%      1.00 ± 0%     ~     (all equal)

The benchtime and count in my benchmarks are unnecessarily high, I get similar results with a lower count and deleted benchtime parameter:

go test -v -bench=BenchmarkIsUnspendable -run=^\$ -benchmem -count 10 >> ~/unspendablenew.txt

This shows that in at least my benchmarks, the allocations and runtime of this method were significantly reduced.
In the future we should definitely backport changes from dcrd #1656 (I will make an issue for this), but that would be a massive PR to consensus critical code, would likely need to be adapted, and the improvements for this method seem good enough for a single, easily reviewable PR.
The point of this PR is to:

  1. Improve allocations of IsUnspendable.
  2. Show that there is significant work to be done on the txscript package, and it would go a very long way towards improving the sync time if we focus on performance using benchmarking and profiling tools.

This is a draft right now because a test isn't passing but this will be fixed soon.

Backporting the benchmark changes would be a great future addition in itself, and hopefully this PR highlights this.

@Rjected
Copy link
Collaborator Author

Rjected commented Aug 19, 2020

New benchmark results for commit b085b02:

name             old time/op    new time/op    delta
IsUnspendable-8     283ns ± 3%       9ns ± 1%   -96.82%  (p=0.000 n=28+28)

name             old alloc/op   new alloc/op   delta
IsUnspendable-8      768B ± 0%        0B       -100.00%  (p=0.000 n=30+30)

name             old allocs/op  new allocs/op  delta
IsUnspendable-8      1.00 ± 0%      0.00       -100.00%  (p=0.000 n=30+30)

Since the tests also pass now, I'll make this a non draft. I'd also like to note that the benchmark is not an authoritative measure of performance. We should build methods that help build benchmarks which represent more of a real world load, take these results with a grain of salt!

@Rjected Rjected marked this pull request as ready for review August 19, 2020 02:59
@jakesylvestre jakesylvestre self-requested a review August 31, 2020 19:32
Copy link
Collaborator

@onyb onyb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK. Good find and quite clever. 👍

// return the list of opcodes up until the point of failure so that this can be
// used in functions which do not necessarily have a need for the failed list of
// opcodes, such as IsUnspendable.
// Not returning the full opcode list up until failure also has the benefit of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want an additional new line here.

Comment on lines 288 to 291
// A script of length zero is an unspendable script but it is parseable.
if len(script) == 0 {
return 0, 0x00, nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already taken care of by the following two lines?

var firstOpcode byte = OP_0
var numParsedInstr uint = 0

The comment still deserves a mention.

return 0, 0x00, nil
}

var firstOpcode byte = OP_0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a misuse of OP_0. If I understand correctly, using any opcode will work here as long as it is not OP_RETURN.

Comment on lines 959 to 964
numberOfPops, firstOpcode, err := checkScriptTemplateParseable(pkScript, &opcodeArray)
if err != nil {
return true
}

return len(pops) > 0 && pops[0].opcode.value == OP_RETURN
return numberOfPops > 0 && firstOpcode == OP_RETURN
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using (*byte, error) as the return type of checkScriptTemplateParseable? The check would simply then be:

firstOpcode, err := checkScriptTemplateParseable(pkScript, &opcodeArray)
if err != nil {
         return true
}

return firstOpcode != nil && *firstOpcode == OP_RETURN

This takes into account my other remark that if the script is empty, it doesn't make sense for the first opcode to be OP_0.

@@ -851,10 +952,14 @@ func getWitnessSigOps(pkScript []byte, witness wire.TxWitness) int {
// guaranteed to fail at execution. This allows inputs to be pruned instantly
// when entering the UTXO set.
func IsUnspendable(pkScript []byte) bool {
pops, err := parseScript(pkScript)
// Not provably unspendable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? It appears to me that it is already taken care of in checkScriptTemplateParseable.

},
{
// Unspendable
pkScript: []byte{0x6a},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pkScript: []byte{0x6a},
pkScript: []byte{OP_RETURN},

},
{
// Spendable
pkScript: []byte{0x51},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pkScript: []byte{0x51},
pkScript: []byte{OP_TRUE},

@@ -4301,6 +4301,28 @@ func TestIsUnspendable(t *testing.T) {
0xfa, 0x0b, 0x5c, 0x88, 0xac},
expected: false,
},
{
// Spendable
pkScript: []byte{0xa9, 0x14, 0x82, 0x1d, 0xba, 0x94, 0xbc, 0xfb,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whenever possible, it's nice to use human-friendly names for the opcodes. It's not necessary in this example, but I added some suggestions for the other test cases you added.

op := &opcodes[instr]
pop := parsedOpcode{opcode: op}

// Parse data out of instruction.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire switch block appears to be duplicated in parseScriptTemplate. Do you think it's worth doing a refactoring?

@onyb onyb added this to To review in btcd-dev Sep 7, 2020
@onyb onyb added this to the 0.22.0 milestone Sep 7, 2020
Copy link
Member

@jcvernaleo jcvernaleo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great but needs to address @onyb's comments then it can go in.

@jcvernaleo
Copy link
Member

I think this needs to be rebased for the proper checks too.

@Rjected Rjected force-pushed the improve-gc-isunspendable branch 3 times, most recently from 9266f14 to 12cc256 Compare December 16, 2020 21:26
@coveralls
Copy link

coveralls commented Dec 16, 2020

Pull Request Test Coverage Report for Build 426674894

  • 81 of 82 (98.78%) changed or added relevant lines in 2 files are covered.
  • 19 unchanged lines in 4 files lost coverage.
  • Overall coverage increased (+1.7%) to 53.402%

Changes Missing Coverage Covered Lines Changed/Added Lines %
txscript/script.go 29 30 96.67%
Files with Coverage Reduction New Missed Lines %
txscript/script.go 1 89.63%
btcec/signature.go 3 92.48%
database/ffldb/blockio.go 4 92.62%
peer/peer.go 11 75.71%
Totals Coverage Status
Change from base Build 407350020: 1.7%
Covered Lines: 20851
Relevant Lines: 39045

💛 - Coveralls

 - create benchmarks to measure allocations
 - add test for benchmark input
 - create a low alloc parseScriptTemplate
 - refactor parsing logic for a single opcode
@jcvernaleo
Copy link
Member

@onyb could you take another look at this one to see if your comments were addressed?

btcd-dev automation moved this from Pending reviews to Reviewer approved Feb 2, 2021
Copy link
Member

@jcvernaleo jcvernaleo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me too now.
OK

@jcvernaleo jcvernaleo merged commit 77fd967 into btcsuite:master Feb 2, 2021
btcd-dev automation moved this from Reviewer approved to Done Feb 2, 2021
@onyb onyb mentioned this pull request Feb 2, 2021
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
btcd-dev
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

4 participants