-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
feat(sdk.Context): Context.KVStore/TransientStore improve performance #14203
feat(sdk.Context): Context.KVStore/TransientStore improve performance #14203
Conversation
There's also something else, for instance applying a pointer receiver on |
Can we switch to pointer receivers here too @testinginprod ? |
Updated to use pointer receiver in KVStore and TransientStore. NOTE: calling |
@aaronc @alexanderbez took execution time down to 64ns by using gasMeter field instead of GasMeter method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a breaking CHANGELOG entry. My preference would be to just convert to a pointer value internally within context and leave the public API unchanged
x/group/internal/orm/testsupport.go
Outdated
gasConfig := storetypes.KVGasConfig() | ||
return gaskv.NewStore(store, g.GasMeter, gasConfig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this offer any performance improvement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the perf improvement is this: https://github.com/cosmos/cosmos-sdk/pull/14203/files#diff-abad3e7f81deb0a089f7e974e60a081e67a7b21e5c6ba5c7b2be3785a66445afR15
this allows us to save time because of alloc + copy, gasKV config is static for a specific context, meaning that as the context is running the gas configuration (how much we spend on each Read/Write/Iter/etc) does not change, so this is why the copy is unnecessary. To further support this point, the only way to modify the GasConfig is by calling WithGasConfig
in Context
which yields to a Context
copy which means the behaviour before my changes is that: changes to GasConfig
are not propagated to existing gaskv
instances. My perf improvement retains this behaviour.
My benches showed it saved a further 10ns
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this allows us to save time because of alloc + copy
Cool, where can we see the benchmark results that demonstrate the perf increases? It would be super cool if changing the representation of this ~50 byte struct from a value to a pointer managed to produce performance benefits from the reduced copy costs that outweighed the additional costs it incurs on the allocator and GC.
Is the claim here that changing the gasConfig field of the KVStore struct from a value to a pointer represents a meaningful improvement in performance? If so, impressive! Can we see the benchstat deltas? |
func (c Context) KVStore(key storetypes.StoreKey) KVStore { | ||
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), c.kvGasConfig) | ||
// NOTE: Uses pointer receiver to save on execution time. | ||
func (c *Context) KVStore(key storetypes.StoreKey) KVStore { | ||
kv := c.ms.GetKVStore(key) | ||
return gaskv.NewStore(kv, c.gasMeter, c.kvGasConfig) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Callers who invoke c.KVStore will get a KVStore value with a kvGasConfig that is no longer a copy of the config data, but instead is a pointer to the same storetypes.GasConfig value of the Context from which the KVStore was derived. This is a fundamental change to the semantics of the KVStore method, and of the Context API in general. It's important to establish the safety of large changes like this with comprehensive test coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ref: #14203 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 if we need testing for this but old behaviour is retained
@peterbourgon yes sir...
The two hashes refer to the commits former should be
current branch:
|
Neat! It seems like this benchmark is measuring the overhead of calling methods on value receivers vs. pointer receivers. It's definitely true that types of size larger than a pointer will copy more data, and burn more CPU, in the former case vs. the latter. Looks like here it's worth about 100ns per call to KVStore. But is that meaningful? What calls KVStore, and how often, and what does that call represent in terms of the high-level operation(s) which invoke it? Does subtracting 100ns from this specific call improve system performance in a measurable way? The PR modifies the semantics of the Context API, and introduces potential concurrency issues — are those modifications tested and ensured to be safe? |
This is a fair point, and I will add behavioural tests. But yeah the modifications made are safe. Then with regards to performance benefit, I think the change is trivial, easy to understand and easy to interpret the consequences (none, besides what mentioned above). Now this is a starting point, I think;
Besides txs,
I cannot provide you the exact amount of times ctx is passed around and the exact number of times methods are being called on it... But my biggest guess is: it's the second most busy type in the SDK, close to |
I agree that the current implementation, which passes sdk.Context around as a value, is weird and bad. But you can't just assert that it's "too big to be passed around like this" as some presumptive claim 😉 First and foremost, because the entire Cosmos ecosystem basically assumes that Context methods are defined on value copies and can't mutate their receiver, and so changing any method to a pointer receiver will have enormous, potentially state- and consensus-breaking, impacts which are absolutely not captured by tests. But also, because the performance problems in the SDK — which are important and serious and worth solving! — are absolutely not influenced by changes at this level. Making an ABCI query traverses, last I checked, almost 36 layers of abstraction, and locks at least 3 exclusive mutexes. Reducing copy costs at the level of tens- or hundreds-of-nanoseconds is an exercise in code golfing with no measurable impact on the bottom line. |
Terrific! Where are the benchmarks, flamegraphs, or any profiling data? I was asked to provide mine and I did, now it's your turn to provide yours 😉 .
If you can make a [concrete] example for which the changes proposed can in any way mutate, or break some behaviour I'd be happy to retract my PR. |
I agree with @peterbourgon that this is a significant behavior change, but I don't mean it should be blocked, but the community should be aware of the consequences. |
closing in favor of: #14354 |
Description
partially addresses: #14202
new execution time:
Author Checklist
All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.
I have...
!
to the type prefix if API or client breaking changeCHANGELOG.md
Reviewers Checklist
All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.
I have...
!
in the type prefix if API or client breaking change