-
-
Notifications
You must be signed in to change notification settings - Fork 455
Improve Performance of the Type System #824
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
base: master
Are you sure you want to change the base?
Conversation
Awesome, it's really interesting to see the results. I think this will be very beneficial for users who use Eval path and do not precompile expressions. |
Type reflect.Type // Type of the value. If nil, then value is unknown. | ||
Kind reflect.Kind // Kind of the value. | ||
|
||
cache *Cache |
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.
As this cache is shared, let's do not set it inside the nature itself, but pass it from the top, from the checker as function arguments.
We already have this problem and we have to set a nature cache, for example, in the following line:
base := n.Node.Nature()
base.SetCache(c) // AST doesn't cache nature info
base = base.Deref()
Let's not set cash inside an agent. But pass in to all functions what may use it:
base := n.Node.Nature()
base = base.Deref(c) // As the deref can modify the cache.
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.
I used your approach in the first version, but it was very difficult to use.
I tried two ways:
- Modify the signature of the methods to pass a
*Cache
as first argument, which can be nil. - Call
SetCache
every time I needed it to ensure it was used.
The problems I found:
- There is no reason to not use the cache, so I think it would be ok to have it be a part.
- The logic to account for cache not being set every time creates a lot of complexity. Remember this happens for every method. I have to look for a suitable alternative in every case. This makes it hard to follow and verify every case.
The solution I came to:
- The cache is not agent, it is also functional. It is integral to Nature. AST only creates raw Nature, but then in order to have type information we use the cache.
- The cache is a proxy but it also makes it easier to centralize all the logic. This makes it easier to implement.
- The cache should be local to a
conf.Config
. This is because that's the best way to reuse it throughout the lifetime of a program being compiled. Starting with the type checker, all following compilation steps need it. - In the early stages when we are parsing the program we are just building the AST so we only care about creating the basics. For example, we don't know yet what type is returned at this point. But then we bind the AST to a cache then it means we are finally interested in type information details.
- I think "bind" is a better word so I changed from
SetCache
toBind
to express this relationship. - The cache is not only meant for the type checker. I only changed mainly the type checker because it's the core, but it is also meant to be used by the optimizer, patchers, and the compiler. I also saw that we could probably use it in the vm and in the runtime because this would avoid some costly reflection checks.
- I didn't go into more usage of it because I made many changes already. I would like to see if these changes are ok and then take a look at the other packages.
We already have this problem and we have to set a nature cache, for example, in the following line:
So as I explain above, this is not a problem. In AST we don't care about type information, we only know about raw elements. So when we need more information we need to bind it with Bind
to a cache.
@@ -34,6 +34,7 @@ type Config struct { | |||
Functions FunctionsTable | |||
Builtins FunctionsTable | |||
Disabled map[string]bool // disabled builtins | |||
NtCache nature.Cache |
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.
Same here, if we can pass it via the function inside the compiler, from the top it will be easier and we don't need to set it here.
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.
See the other comment.
Nice -38.86% improvments. 👍🏻 |
I updated the benchmarks, now it's ~58%!
This would also be useful in tight loops when we need to compile many expressions, for example when a service initializes. |
// and lexer do not need to use the cache because they don't need any service | ||
// from the Nature type, they only describe. However, when receiving a Nature | ||
// from one of those packages, the cache must be set immediately. | ||
type Cache struct { |
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.
Can we use global cache instance under sync.RWLock? Will it make core more easy to maintain?
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.
I thought of different alternatives but they seem very complicated. I will try passing Cache from the top as you suggested above.
|
||
cache *Cache | ||
*Optional | ||
*FuncData |
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.
Please also write the comment on Why? do we need separate *Optional and *FuncData so reader will understand it.
|
||
*structData | ||
|
||
// map-only data | ||
Fields map[string]Nature // Fields of map type. |
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.
If we avoiding map in methodset , why this map?
if n.Type != nil { | ||
n.Type = deref.Type(n.Type) | ||
func (c *Cache) getStruct(t reflect.Type) Nature { | ||
if c != nil { |
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.
How can c
be nil here? Looks like redundent if.
// Untyped nil is assignable to any interface, but implements only the empty interface. | ||
if nt.IsAny() { | ||
switch nt.Kind { | ||
case reflect.Pointer, reflect.Interface: |
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.
Checking for reflect.Interfece is not sufficient. We need to nt.IsAny()
:
func (n Nature) IsAny() bool {
return n.Kind() == reflect.Interface && n.NumMethods() == 0
return nt | ||
} | ||
|
||
func (n *Nature) Bind(c *Cache) { |
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 is hard to follow logic. Too many ifs and branching. Why do we need to write cache on bind op?
Alos if Nature relies on Cache, lets make it impossible to create nature without the Cache?
Improvements to the Type System:
Nature
work on*Nature
instead. This avoid excessive memory copying.checker
tonature
because this prevents too much memory copying.staticcheck
andgolangci-lint
).Details for the future:
Nature
as value whenever possible. I made the methods work on the pointer because it avoids excessive copying that I saw in the profiling.*Cache
is mandatory only for complex operations. Inast
andparser
it's ok to not use it because there we don't need anything special.