Understanding when @inlinable is needed for generic specialization

suppose i have an application with a dependency graph that looks like:

.target("FrameworkCore"),
.target("Framework", 
    dependencies:
    [
        "FrameworkCore", 
        .product(name: "NIOCore", package: "swift-nio")
    ]),
.target("ApplicationCore", dependencies: ["FrameworkCore"]),
.target("Application", dependencies: ["ApplicationCore", "Framework"]),

FrameworkCore

the FrameworkCore target defines a MyDecodingContainer<Storage> type, which is generic over some storage type, whose subscript(_:) will be called often.

this type is @frozen, and all of its API is @inlinable.

it also has a public MyDecodable protocol, which has a generic init(from:) requirement taking an instance of MyDecodingContainer.

FrameworkCore/MyDecodable.swift

@frozen public
struct MyDecodingContainer<Storage>
    where Storage:RandomAccessCollection<UInt8>
{
    ...
}

public
protocol MyDecodable
{
    init(from:MyDecodingContainer<some RandomAccessCollection<UInt8>>)
        throws
}

Framework

the Framework target contains a type MyGenericScheme, which is generic over some MyDecodable type, and uses a specialization of MyDecodable and MyDecodingContainers’ API where Storage == NIOCore.ByteBufferView.

MyGenericScheme is not @frozen, and its API is not @inlinable, at least not yet.

Framework depends on NIOCore, and NIOCore takes a long time to clone and build. and it’s also annoying to repeatedly import NIOCore everywhere, so targets that can depend on FrameworkCore only should depend on FrameworkCore and not Framework.

Framework/MyGenericScheme.swift

import FrameworkCore
import NIOCore

public
struct MyGenericScheme<Element> where Element:MyDecodable
{
    public
    let element:Element
}
extension MyGenericScheme
{
    public
    init(bytes:ByteBufferView) throws
    {
        let decoder:MyDecodingContainer<ByteBufferView> = .init(bytes)
        self.init(element: try .init(from: decoder))
    }
}

ApplicationCore

ApplicationCore is a application-specific target that defines a type Foo that conforms to FrameworkCore.MyDecodable.

ApplicationCore does not depend on NIOCore, and knows nothing about ByteBuffer or ByteBufferView. so clearly, Foo’s MyDecodable conformance must be @inlinable in order to get anything resembling decent performance.

ApplicationCore/Foo.swift

import FrameworkCore

@frozen public
struct Foo
{
}

extension Foo:MyDecodable
{
    @inlinable public
    init(from:MyDecodingContainer<some RandomAccessCollection<UInt8>>)
        throws
    {
        ...
    }
}

Application

finally, Application brings the four components (ApplicationCore, FrameworkCore, Framework, and NIOCore) together, with a function bar(bytes:) like the one below:

Application/Bar.swift

import ApplicationCore
import FrameworkCore
import Framework
import NIOCore

func bar(bytes:ByteBufferView) throws -> Foo
{
    let scheme:MyGenericScheme<Foo> = try .init(bytes: bytes)
    return scheme.element
}

my question is about how resilient MyGenericScheme can be, while still maintaining acceptable performance.

  1. does MyGenericScheme.init(from:) need to become @inlinable?
  2. does MyGenericScheme itself need to become @frozen?

If you want generic specialisation, yes. Otherwise, the implementation of init is unknown and cannot be specialised for the usage site.

No. @frozen has limited utility in non-resilient libraries, and nothing here seems like it would be affected.

1 Like

hmm. that’s unfortunate, sometimes i feel like i am just marking everything @inlinable by default.

i don’t understand what you mean by this, isn’t @frozen needed to be able to access MyGenericScheme.element without going through a getter?

This only occurs in resilient modules.

2 Likes

In principle, you can avoid this by using cross-module optimisation.

But with care, see e.g.

(I'll admit we haven't tried it again since April, so maybe something was fixed in 5.7)

1 Like