Motivation
The current implementation of functions related to Codable types lacks the ability to be inlined.
For example:
- Encodable
func encode(to encoder: Encoder) throws
assumesany Encoder
. - Decodable
init(from decoder: Decoder) throws
assumesany Decoder
. - Functions of
KeyedEncodingContainer
,_KeyedEncodingContainerBase
and_KeyedEncodingContainerBox
,KeyedDecodingContainer
,_KeyedDecodingContainerBase
,_KeyedDecodingContainerBox
aren't marked with@inlinable
. - Extensions of builtin types (
String
,Bool
,Int
, etc) toCodable
aren't inlinable as well.
This leads to sub-optimal code generation by the compiler, resulting in a potential performance bottleneck when encoding and decoding in a hotpath.
Solution
The final goal here is to allow the optimizer to do its job as good as it can. For example, when calling myStruct.encode(to: myEncoder)
, the resulting code should be free from boxing, jumping back and forth to/from libswiftCore, calls to __swift_instantiateConcreteTypeFromMangledName
, etc. We need just direct access to the fields of the struct and static dispatch to the writing methods of the encoder.
To achieve this, we need at least to:
- Mark all functions with the
@inlinable
attribute. - Use generics in
init(from decoder: Decoder) throws
andfunc encode(to encoder: Encoder) throws
. - Probably need to mark synthesized implementations of
init(from:)
andencode(to:)
with@inlinable
as well.
Alternatives considered
Probably the whole Codable system can be reinvented with Macros with more precise control on the user code side, not the stdlib side.
Additional context
GitHub issue
Prototyping
I made two demos.
The first one uses stdlib's Encodable
.
The second one uses a copy of Encodable
with slightly tweaked signatures.
Both do the same: encode a struct of two fields to an encoder that calls uninlinable function to print args.
As you can see the first one does:
call (lazy protocol witness table accessor for type output._Encoder and conformance output._Encoder : Swift.Encoder in output)
call (output.Foo.encode(to: Swift.Encoder) throws -> ())
call __swift_instantiateConcreteTypeFromMangledName
call __swift_project_boxed_opaque_existential_1
call (lazy protocol witness table accessor for type output.Foo.(CodingKeys in _60494E8B9C642A7C4A26F3A3B6CECEB9) and conformance output.Foo.(CodingKeys in _60494E8B9C642A7C4A26F3A3B6CECEB9) : Swift.CodingKey in output)
call ($ss7EncoderP9container7keyedBys22KeyedEncodingContainerVyqd__Gqd__m_ts9CodingKeyRd__lFTj)@PLT
call ($ss22KeyedEncodingContainerV6encode_6forKeyySi_xtKF)@PLT
call (type metadata accessor for output._KeyedEncodingContainer)
call swift_getWitnessTable@PLT
call ($ss9CodingKeyP11stringValueSSvgTj)@PLT
And more in stdlib. While the second one:
call __swift_instantiateConcreteTypeFromMangledName
call swift_initStackObject@PLT
And then two direct calls to the print function. And I'm not sure why there are calls to __swift_instantiateConcreteTypeFromMangledName
and swift_initStackObject
here.