Skip to content

Commit a94e30b

Browse files
committed
[Serialization] fix MethodTable/Cache serializations
Missed updates from early designs in #58131.
1 parent f24d939 commit a94e30b

File tree

3 files changed

+39
-67
lines changed

3 files changed

+39
-67
lines changed

doc/src/devdocs/functions.md

Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,17 @@ This document will explain how functions, method definitions, and method tables
66
## Method Tables
77

88
Every function in Julia is a generic function. A generic function is conceptually a single function,
9-
but consists of many definitions, or methods. The methods of a generic function are stored in
10-
a method table. Method tables (type `MethodTable`) are associated with `TypeName`s. A `TypeName`
11-
describes a family of parameterized types. For example `Complex{Float32}` and `Complex{Float64}`
12-
share the same `Complex` type name object.
13-
14-
All objects in Julia are potentially callable, because every object has a type, which in turn
15-
has a `TypeName`.
9+
but consists of many definitions, or methods. The methods of a generic function are stored in a
10+
method table. There is one global method table (type `MethodTable`) named `Core.GlobalMethods`. Any
11+
default operation on methods (such as calls) uses that table.
1612

1713
## [Function calls](@id Function-calls)
1814

19-
Given the call `f(x, y)`, the following steps are performed: first, the method cache to use is
20-
accessed as `typeof(f).name.mt`. Second, an argument tuple type is formed, `Tuple{typeof(f), typeof(x), typeof(y)}`.
21-
Note that the type of the function itself is the first element. This is because the type might
22-
have parameters, and so needs to take part in dispatch. This tuple type is looked up in the method
23-
table.
15+
Given the call `f(x, y)`, the following steps are performed: First, a tuple type is formed,
16+
`Tuple{typeof(f), typeof(x), typeof(y)}`. Note that the type of the function itself is the first
17+
element. This is because the function itself participates symmetrically in method lookup with the
18+
other arguments. This tuple type is looked up in the global method table. However, the system can
19+
then cache the results, so these steps can be skipped later for similar lookups.
2420

2521
This dispatch process is performed by `jl_apply_generic`, which takes two arguments: a pointer
2622
to an array of the values `f`, `x`, and `y`, and the number of values (in this case 3).
@@ -49,15 +45,6 @@ jl_value_t *jl_call(jl_function_t *f, jl_value_t **args, int32_t nargs);
4945

5046
Given the above dispatch process, conceptually all that is needed to add a new method is (1) a
5147
tuple type, and (2) code for the body of the method. `jl_method_def` implements this operation.
52-
`jl_method_table_for` is called to extract the relevant method table from what would be
53-
the type of the first argument. This is much more complicated than the corresponding procedure
54-
during dispatch, since the argument tuple type might be abstract. For example, we can define:
55-
56-
```julia
57-
(::Union{Foo{Int},Foo{Int8}})(x) = 0
58-
```
59-
60-
which works since all possible matching methods would belong to the same method table.
6148

6249
## Creating generic functions
6350

@@ -94,9 +81,7 @@ end
9481
9582
## Constructors
9683
97-
A constructor call is just a call to a type. The method table for `Type` contains all
98-
constructor definitions. All subtypes of `Type` (`Type`, `UnionAll`, `Union`, and `DataType`)
99-
currently share a method table via special arrangement.
84+
A constructor call is just a call to a type, to a method defined on `Type{T}`.
10085
10186
## Builtins
10287
@@ -128,18 +113,14 @@ import Markdown
128113
Markdown.parse
129114
```
130115
131-
These are all singleton objects whose types are subtypes of `Builtin`, which is a subtype of
132-
`Function`. Their purpose is to expose entry points in the run time that use the "jlcall" calling
133-
convention:
116+
These are mostly singleton objects all of whose types are subtypes of `Builtin`, which is a
117+
subtype of `Function`. Their purpose is to expose entry points in the run time that use the
118+
"jlcall" calling convention:
134119
135120
```c
136121
jl_value_t *(jl_value_t*, jl_value_t**, uint32_t)
137122
```
138123
139-
The method tables of builtins are empty. Instead, they have a single catch-all method cache entry
140-
(`Tuple{Vararg{Any}}`) whose jlcall fptr points to the correct function. This is kind of a hack
141-
but works reasonably well.
142-
143124
## Keyword arguments
144125
145126
Keyword arguments work by adding methods to the kwcall function. This function
@@ -228,18 +209,13 @@ sees an argument in the `Function` type hierarchy passed to a slot declared as `
228209
it behaves as if the `@nospecialize` annotation were applied. This heuristic seems to be extremely
229210
effective in practice.
230211
231-
The next issue concerns the structure of method cache hash tables. Empirical studies show that
232-
the vast majority of dynamically-dispatched calls involve one or two arguments. In turn, many
233-
of these cases can be resolved by considering only the first argument. (Aside: proponents of single
234-
dispatch would not be surprised by this at all. However, this argument means "multiple dispatch
235-
is easy to optimize in practice", and that we should therefore use it, *not* "we should use single
236-
dispatch"!) So the method cache uses the type of the first argument as its primary key. Note,
237-
however, that this corresponds to the *second* element of the tuple type for a function call (the
238-
first element being the type of the function itself). Typically, type variation in head position
239-
is extremely low -- indeed, the majority of functions belong to singleton types with no parameters.
240-
However, this is not the case for constructors, where a single method table holds constructors
241-
for every type. Therefore the `Type` method table is special-cased to use the *first* tuple type
242-
element instead of the second.
212+
The next issue concerns the structure of method tables. Empirical studies show that the vast
213+
majority of dynamically-dispatched calls involve one or two arguments. In turn, many of these cases
214+
can be resolved by considering only the first argument. (Aside: proponents of single dispatch would
215+
not be surprised by this at all. However, this argument means "multiple dispatch is easy to optimize
216+
in practice", and that we should therefore use it, *not* "we should use single dispatch"!). So the
217+
method table and cache splits up on the structure based on a left-to-right decision tree so allow
218+
efficient nearest-neighbor searches.
243219
244220
The front end generates type declarations for all closures. Initially, this was implemented by
245221
generating normal type declarations. However, this produced an extremely large number of constructors,

doc/src/devdocs/types.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,12 @@ julia> dump(Array{Int,1}.name)
176176
TypeName
177177
name: Symbol Array
178178
module: Module Core
179-
names: empty SimpleVector
179+
singletonname: Symbol Array
180+
names: SimpleVector
181+
1: Symbol ref
182+
2: Symbol size
183+
atomicfields: Ptr{Nothing}(0x0000000000000000)
184+
constfields: Ptr{Nothing}(0x0000000000000000)
180185
wrapper: UnionAll
181186
var: TypeVar
182187
name: Symbol T
@@ -188,20 +193,20 @@ TypeName
188193
lb: Union{}
189194
ub: abstract type Any
190195
body: mutable struct Array{T, N} <: DenseArray{T, N}
196+
Typeofwrapper: abstract type Type{Array} <: Any
191197
cache: SimpleVector
192198
...
193-
194199
linearcache: SimpleVector
195200
...
196-
197-
hash: Int64 -7900426068641098781
198-
mt: MethodTable
199-
name: Symbol Array
200-
defs: Nothing nothing
201-
cache: Nothing nothing
202-
module: Module Core
203-
: Int64 0
204-
: Int64 0
201+
hash: Int64 2594190783455944385
202+
backedges: #undef
203+
partial: #undef
204+
max_args: Int32 0
205+
n_uninitialized: Int32 0
206+
flags: UInt8 0x02
207+
cache_entry_count: UInt8 0x00
208+
max_methods: UInt8 0x00
209+
constprop_heuristic: UInt8 0x00
205210
```
206211

207212
In this case, the relevant field is `wrapper`, which holds a reference to the top-level type used

stdlib/Serialization/src/Serialization.jl

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -469,15 +469,13 @@ end
469469

470470
function serialize(s::AbstractSerializer, mt::Core.MethodTable)
471471
serialize_type(s, typeof(mt))
472-
serialize(s, mt.cache)
472+
serialize(s, mt.name)
473+
serialize(s, mt.module)
473474
nothing
474475
end
475476

476477
function serialize(s::AbstractSerializer, mc::Core.MethodCache)
477-
serialize_type(s, typeof(mc))
478-
serialize(s, mc.name)
479-
serialize(s, mc.module)
480-
nothing
478+
error("cannot serialize MethodCache objects")
481479
end
482480

483481

@@ -1134,16 +1132,9 @@ function deserialize(s::AbstractSerializer, ::Type{Method})
11341132
end
11351133

11361134
function deserialize(s::AbstractSerializer, ::Type{Core.MethodTable})
1137-
mc = deserialize(s)::Core.MethodCache
1138-
mc === Core.GlobalMethods.cache && return Core.GlobalMethods
1139-
return getglobal(mc.mod, mc.name)::Core.MethodTable
1140-
end
1141-
1142-
function deserialize(s::AbstractSerializer, ::Type{Core.MethodCache})
11431135
name = deserialize(s)::Symbol
11441136
mod = deserialize(s)::Module
1145-
f = Base.unwrap_unionall(getglobal(mod, name))
1146-
return (f::Core.MethodTable).cache
1137+
return getglobal(mc.mod, mc.name)::Core.MethodTable
11471138
end
11481139

11491140
function deserialize(s::AbstractSerializer, ::Type{Core.MethodInstance})

0 commit comments

Comments
 (0)