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

Refactor external types to instantiate only once whenever possible #2225

Merged
merged 9 commits into from
Feb 1, 2023

Conversation

SupunS
Copy link
Member

@SupunS SupunS commented Jan 6, 2023

Depends on: #2248

Description

New instances of Cadence external types are created every time during decoding, type conversions, etc.
However, some of them (simple types) can be instantiated only once and can be re-used, which also eliminates the need for memory metering. Hence refactored such types by introducing and re-using global instances.

Also noticed that cadence.ExportType method is a duplicate of cadence.ExportMeteredType with the only difference being the memory gauge is nil. Refactored that as well, to re-use one method for both scenarios.


  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work
  • Code follows the standards mentioned here
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels

@SupunS SupunS self-assigned this Jan 6, 2023
Comment on lines -32 to -35
func ExportType(
t sema.Type,
results map[sema.TypeID]cadence.Type,
) cadence.Type {
Copy link
Member Author

Choose a reason for hiding this comment

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

This method was a duplicate of ExportMeteredType(nil, t, results)

@codecov
Copy link

codecov bot commented Jan 6, 2023

Codecov Report

Merging #2225 (699bc1d) into master (559e775) will increase coverage by 0.01%.
The diff coverage is 44.96%.

@@            Coverage Diff             @@
##           master    #2225      +/-   ##
==========================================
+ Coverage   77.77%   77.79%   +0.01%     
==========================================
  Files         311      311              
  Lines       65970    65680     -290     
==========================================
- Hits        51311    51097     -214     
+ Misses      12889    12815      -74     
+ Partials     1770     1768       -2     
Flag Coverage Δ
unittests 77.79% <44.96%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
runtime/common/memorykind_string.go 40.00% <ø> (ø)
runtime/common/metering.go 92.23% <ø> (ø)
types.go 79.20% <0.00%> (-14.74%) ⬇️
values.go 72.93% <48.93%> (+6.34%) ⬆️
runtime/convertTypes.go 84.66% <76.92%> (+15.80%) ⬆️
encoding/json/decode.go 88.12% <94.11%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@github-actions
Copy link

github-actions bot commented Jan 6, 2023

Cadence Benchstat comparison

This branch with compared with the base branch onflow:master commit 559e775
The command for i in {1..N}; do go test ./... -run=XXX -bench=. -benchmem -shuffle=on; done was used.
Bench tests were run a total of 7 times on each branch.

Collapsed results for better readability

old.txtnew.txt
time/opdelta
CheckContractInterfaceFungibleTokenConformance-2108µs ± 0%108µs ± 0%~(p=1.000 n=1+1)
ContractInterfaceFungibleToken-237.4µs ± 0%37.3µs ± 0%~(p=1.000 n=1+1)
InterpretRecursionFib-22.47ms ± 0%2.45ms ± 0%~(p=1.000 n=1+1)
NewInterpreter/new_interpreter-21.07µs ± 0%1.08µs ± 0%~(p=1.000 n=1+1)
NewInterpreter/new_sub-interpreter-2569ns ± 0%572ns ± 0%~(p=1.000 n=1+1)
ParseArray-27.90ms ± 0%7.56ms ± 0%~(p=1.000 n=1+1)
ParseDeploy/byte_array-211.9ms ± 0%11.1ms ± 0%~(p=1.000 n=1+1)
ParseDeploy/decode_hex-21.24ms ± 0%1.18ms ± 0%~(p=1.000 n=1+1)
ParseFungibleToken/With_memory_metering-2185µs ± 0%180µs ± 0%~(p=1.000 n=1+1)
ParseFungibleToken/Without_memory_metering-2143µs ± 0%143µs ± 0%~(p=1.000 n=1+1)
ParseInfix-27.15µs ± 0%7.02µs ± 0%~(p=1.000 n=1+1)
QualifiedIdentifierCreation/One_level-22.62ns ± 0%2.41ns ± 0%~(p=1.000 n=1+1)
QualifiedIdentifierCreation/Three_levels-2118ns ± 0%114ns ± 0%~(p=1.000 n=1+1)
RuntimeResourceDictionaryValues-25.07ms ± 0%5.12ms ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-24.00µs ± 0%3.98µs ± 0%~(p=1.000 n=1+1)
SuperTypeInference/arrays-2290ns ± 0%286ns ± 0%~(p=1.000 n=1+1)
SuperTypeInference/composites-2129ns ± 0%128ns ± 0%~(p=1.000 n=1+1)
SuperTypeInference/integers-292.8ns ± 0%93.5ns ± 0%~(p=1.000 n=1+1)
ValueIsSubtypeOfSemaType-287.5ns ± 0%88.5ns ± 0%~(p=1.000 n=1+1)
 
alloc/opdelta
CheckContractInterfaceFungibleTokenConformance-249.2kB ± 0%49.2kB ± 0%~(all equal)
ContractInterfaceFungibleToken-223.1kB ± 0%23.1kB ± 0%~(all equal)
InterpretRecursionFib-21.00MB ± 0%1.00MB ± 0%~(p=1.000 n=1+1)
NewInterpreter/new_interpreter-2768B ± 0%768B ± 0%~(all equal)
NewInterpreter/new_sub-interpreter-2200B ± 0%200B ± 0%~(all equal)
ParseArray-22.72MB ± 0%2.79MB ± 0%~(p=1.000 n=1+1)
ParseDeploy/byte_array-24.09MB ± 0%4.09MB ± 0%~(p=1.000 n=1+1)
ParseDeploy/decode_hex-2214kB ± 0%214kB ± 0%~(p=1.000 n=1+1)
ParseFungibleToken/With_memory_metering-229.2kB ± 0%29.2kB ± 0%~(all equal)
ParseFungibleToken/Without_memory_metering-229.2kB ± 0%29.2kB ± 0%~(p=1.000 n=1+1)
ParseInfix-21.92kB ± 0%1.91kB ± 0%~(p=1.000 n=1+1)
QualifiedIdentifierCreation/One_level-20.00B 0.00B ~(all equal)
QualifiedIdentifierCreation/Three_levels-264.0B ± 0%64.0B ± 0%~(all equal)
RuntimeResourceDictionaryValues-22.28MB ± 0%2.28MB ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-22.70kB ± 0%2.70kB ± 0%~(all equal)
SuperTypeInference/arrays-296.0B ± 0%96.0B ± 0%~(all equal)
SuperTypeInference/composites-20.00B 0.00B ~(all equal)
SuperTypeInference/integers-20.00B 0.00B ~(all equal)
ValueIsSubtypeOfSemaType-248.0B ± 0%48.0B ± 0%~(all equal)
 
allocs/opdelta
CheckContractInterfaceFungibleTokenConformance-2806 ± 0%806 ± 0%~(all equal)
ContractInterfaceFungibleToken-2370 ± 0%370 ± 0%~(all equal)
InterpretRecursionFib-218.9k ± 0%18.9k ± 0%~(all equal)
NewInterpreter/new_interpreter-213.0 ± 0%13.0 ± 0%~(all equal)
NewInterpreter/new_sub-interpreter-24.00 ± 0%4.00 ± 0%~(all equal)
ParseArray-259.6k ± 0%59.6k ± 0%~(p=1.000 n=1+1)
ParseDeploy/byte_array-289.4k ± 0%89.4k ± 0%~(p=1.000 n=1+1)
ParseDeploy/decode_hex-263.0 ± 0%63.0 ± 0%~(all equal)
ParseFungibleToken/With_memory_metering-2778 ± 0%778 ± 0%~(all equal)
ParseFungibleToken/Without_memory_metering-2778 ± 0%778 ± 0%~(all equal)
ParseInfix-248.0 ± 0%48.0 ± 0%~(all equal)
QualifiedIdentifierCreation/One_level-20.00 0.00 ~(all equal)
QualifiedIdentifierCreation/Three_levels-22.00 ± 0%2.00 ± 0%~(all equal)
RuntimeResourceDictionaryValues-236.9k ± 0%36.9k ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-243.0 ± 0%43.0 ± 0%~(all equal)
SuperTypeInference/arrays-23.00 ± 0%3.00 ± 0%~(all equal)
SuperTypeInference/composites-20.00 0.00 ~(all equal)
SuperTypeInference/integers-20.00 0.00 ~(all equal)
ValueIsSubtypeOfSemaType-21.00 ± 0%1.00 ± 0%~(all equal)
 

@SupunS SupunS marked this pull request as ready for review January 6, 2023 17:37
@SupunS SupunS requested a review from dreamsmasher January 6, 2023 17:37
Copy link
Member

@fxamacker fxamacker left a comment

Choose a reason for hiding this comment

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

Thanks for implementing PathLinkType and great idea to refactor simple types as singletons. 👍

I have a few questions:

  • Should we support encoding and decoding PathLinkType as type value?
  • Should we make new singleton functions such as NewAnyType() unexported to encourage reusing singleton?
  • Should we use pointer for simple type singletons?

@SupunS
Copy link
Member Author

SupunS commented Jan 6, 2023

Thank you for having a look @fxamacker!

Should we support encoding and decoding PathLinkType as type value?

Currently, a PathLinkType cannot exist at Cadence runtime (there is no run-time/internal type counterpart for external PathLinkType). So even if we add encoding/decoding, it will not be used.
I still need to add a test for this; will be adding it with the rest of the tests 👍

Should we make new singleton functions such as NewAnyType() unexported to encourage reusing singleton?

I also had the same thought, but eventually decided to keep it as is, so that any downstream dependencies (e.g: go-sdk, any community project, etc.) would not be broken by the API change. I did remove the metered constructor though, since it is supposed to be used internally only.

Should we use pointer for simple type singletons?

Yeah, the real use of singleton comes if we actually re-use the same memory location. But I'm not sure how will it impact perf (GC in particular). i.e: Go copying in stack vs GC-ing a heap object/pointer. Do you think a pointer would be better performant/memory efficient?
I guess the gain we get from the current refactoring is the removal of metering overhead.

@SupunS SupunS force-pushed the supun/external-types branch from b014471 to db15d9b Compare January 7, 2023 00:01
@fxamacker
Copy link
Member

fxamacker commented Jan 7, 2023

@SupunS

Currently, a PathLinkType cannot exist at Cadence runtime (there is no run-time/internal type counterpart for external PathLinkType). So even if we add encoding/decoding, it will not be used.

Ah, that makes sense. Maybe a comment can be added to document this.

Yeah, the real use of singleton comes if we actually re-use the same memory location. But I'm not sure how will it impact perf (GC in particular). i.e: Go copying in stack vs GC-ing a heap object/pointer. Do you think a pointer would be better performant/memory efficient?

Actually, escape analysis with go1.19.4 shows both non-pointer singleton and pointer singleton escape to the heap. Pointer singleton is automatically escaped to the heap. Non-pointer singleton is escaped because of how it is used by value object. So either approach is OK.

var TheVoidType = NewVoidType()

func NewVoidType() *VoidType {
	return &VoidType{}
}
var TheVoidType = NewVoidType()

func NewVoidType() VoidType {
	return VoidType{}
}

I guess the gain we get from the current refactoring is the removal of metering overhead.

Another benefit of using singletons is memory optimization by reducing heap use. For example, the current implementation of Type() creates a new heap object. So using singleton will avoid dynamic allocations that just create duplicates on the heap.

func (Void) Type() Type {
	return NewVoidType()
}

@SupunS
Copy link
Member Author

SupunS commented Jan 9, 2023

I can also make the constructor return the singleton (instead of removing the constructor), so we can achieve both at once: reducing memory allocations while keeping the API backward compatible.

var TheVoidType = VoidType{}

func NewVoidType() VoidType {
	return TheVoidType
}

Actually, escape analysis with go1.19.4 shows both non-pointer singleton and pointer singleton escape to the heap.

Oh, didn't know that! I was assuming otherwise.

Copy link
Member

@turbolent turbolent left a comment

Choose a reason for hiding this comment

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

👍

types.go Show resolved Hide resolved
@SupunS SupunS force-pushed the supun/external-types branch from 18e9da0 to 5876405 Compare January 12, 2023 16:31
@SupunS SupunS merged commit 772d5d9 into master Feb 1, 2023
@SupunS SupunS deleted the supun/external-types branch February 1, 2023 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants