Pitch: Cross-module inlining and specialization

This semantic doesn’t make sense to me, and I think we need to change it. I think we are better served with the semantics of “the body may be inlined, but doesn’t have to.”

That is the effect it has today. The decision to inline or not is made by the optimizer, and @inlinable doesn’t change anything here; it makes the body available if the optimizer chooses to do so.

Also remember we have the @inline(never) attribute. It’s not underscored so I’m assuming it’s an “official” part of the language. And "@inline(never) @inlinable" is a perfectly valid combination — it serializes the SIL for the function body, and while inlining it is prohibited, it is still subject to specialization, function signature optimizations, etc.

Slava

FWIW, the @inlinable name has always confused me. Methods not marked @inlinable are still internally inlinable. "Inlining" is already a term of art with specific semantics in other languages, and even in Swift is it's own thing to be controlled independently from resilience. The real issue I have with the name is that it says nothing about resilience. I’ll never forget that fragility is the opposite of resilience. I can't see how a @fragile attribute would ever be misconstrued.

+1. This is exactly how I feel.

···

On Oct 3, 2017, at 12:28 AM, Andrew Trick via swift-evolution <swift-evolution@swift.org> wrote:

On Oct 2, 2017, at 11:20 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Oct 2, 2017, at 11:11 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

As for the various shades of fragility of data types, I don't see why that can't be handled as qualifiers or additional optional attributes for expert developers. It’s just a matter of picking a reasonable default.

-Andy
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Is this only a problem with fileprivate or does it extend to private
members too? I feel like this would be a very valuable feature to support.

Private members too. Consider this example,

struct S {
  private func f() {}
}

The member S.f mangles
as _T06struct1SV1f33_AB643CAAAE0894CD0BC8584D7CA3AD23LLyyF. In this case, I
suppose we won’t need the private discriminator because there can only be
one S.f that’s directly a member of S, and not an extension. However
imagine if two different source files both defined extensions of S, with a
private member f. You would need to disambiguate them somehow.

The simple-minded way to do this would be to require @_versioned
annotations on private and fileprivate members to supply an internally
unique alternative name to be used for mangling-as-though-internal (i.e.
`@_versioned(my_extension_f)`). Such a function becoming public in an
ABI-compatible way would require renaming the "actual" name to the unique
@_versioned name.

We have _silgen_name for that, but we really don’t want to expose this
more generally because people have been abusing it to make things visible
to C, and they should be using @_cdecl instead.

The difference here would be that the "@_versioned name" would be subject
to mangling. It's essentially equivalent to a way of specifying a custom
discriminator to be hashed so that the source file name is omitted and not
ABI. Not that I think it'd be elegant, but it would not be abusable like
_silgen_name.

A more elegant refinement could be to have @_versioned private and
fileprivate members mangled as though internal, erroring if two or more
members with the same name are both @_versioned--would that work?

If you’re going to do that what is the value in having the capability at
all?

Solely to have some way of preventing members in one file from calling
members in another file at compile time.

···

On Mon, Oct 2, 2017 at 22:23 Slava Pestov <spestov@apple.com> wrote:

On Oct 2, 2017, at 8:06 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Mon, Oct 2, 2017 at 9:55 PM, Slava Pestov <spestov@apple.com> wrote:

On Oct 2, 2017, at 7:52 PM, Kelvin Ma <kelvin13ma@gmail.com> wrote:

Is this only a problem with fileprivate or does it extend to private members too? I feel like this would be a very valuable feature to support.

Private members too. Consider this example,

struct S {
  private func f() {}
}

The member S.f mangles as _T06struct1SV1f33_AB643CAAAE0894CD0BC8584D7CA3AD23LLyyF. In this case, I suppose we won’t need the private discriminator because there can only be one S.f that’s directly a member of S, and not an extension. However imagine if two different source files both defined extensions of S, with a private member f. You would need to disambiguate them somehow.

The simple-minded way to do this would be to require @_versioned annotations on private and fileprivate members to supply an internally unique alternative name to be used for mangling-as-though-internal (i.e. `@_versioned(my_extension_f)`). Such a function becoming public in an ABI-compatible way would require renaming the "actual" name to the unique @_versioned name.

We have _silgen_name for that, but we really don’t want to expose this more generally because people have been abusing it to make things visible to C, and they should be using @_cdecl instead.

This is really not what _silgen_name is for. I suspect we'll need a mangle-as at some point that doesn't require using a full mangled name, but I don't think this is the place to introduce it.

A more elegant refinement could be to have @_versioned private and fileprivate members mangled as though internal, erroring if two or more members with the same name are both @_versioned--would that work?

If you’re going to do that what is the value in having the capability at all?

I'm actually okay with this, but it's harder to implement. Someone can come along and add it later and we'd be fine; I don't think this solution or any other has to be part of the initial proposal for it to be useful.

Jordan

···

On Oct 2, 2017, at 20:23, Slava Pestov via swift-evolution <swift-evolution@swift.org> wrote:

On Oct 2, 2017, at 8:06 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:
On Mon, Oct 2, 2017 at 9:55 PM, Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

On Oct 2, 2017, at 7:52 PM, Kelvin Ma <kelvin13ma@gmail.com <mailto:kelvin13ma@gmail.com>> wrote:

It wouldn't avoid the complexity, because we want the "non-ABI, always-emit-into-client" behavior for the standard library. For the soon-to-be-ABI-stable libraries where @inlinable even matters, such as the standard library and Apple SDK overlays, there's pretty much perfect overlap between things we want to inline and things we don't want to take up binary space and ABI surface in binaries, so the behavior Slava proposes seems like the right default.

I disagree. The semantics being proposed perfectly overlap with the transitional plan for overlays (which matters for the next few years), but they are the wrong default for anything other than overlays and the wrong thing for long term API evolution over the next 20 years.

Can you elaborate on this? If inlinable functions have public entry points, the version in the framework may or may not be called… because of SIL serialization and inlining. Since the existence of the public entry point doesn’t offer much of a guarantee, it seems desirable to not have the public entry point. For example if the inlinable function is not used elsewhere in the framework, we wouldn’t have to emit it at all. This might make the standard library smaller for instance.

Sure, let me explain the same thing a different way: because the symbol is known to be present, clients are not *forced* to inline the body of the function if it is not profitable. They know that there is always a single canonical implementation of the function, so they aren’t forced to replicate it N times for N clients.

Remember also that there are times where the body of a symbol isn’t the thing that is useful. If you curry a method for example, you want its address. With the proposed inlining semantics, this forces a copy of the body to be emitted in every module that uses it, because you don’t have a canonical name for the thing.

However I’m still waiting for Dave or Jordan to chime in with the original justification for the ‘always emit into client’ behavior. IIRC there was a resilience-related argument too, but I don’t remember what it is now.

The only argument I can imagine is the “If it gets inlined, you’re guaranteed to get the version of the symbol you build against”. The concern is that some instances are inlined and some are not, and if the inline and out of line versions diverge then you can have exciting problems.

My view on that is that you’ve already lost if you’d done this. If you mark a declaration as fragile (allowing it to be inlined) you’ve specifically guaranteed that you’re not going to be changing the observable semantics of the function. Introducing new performance optimizations is fine of course.

-Chris

···

On Oct 3, 2017, at 9:59 PM, Slava Pestov <spestov@apple.com> wrote:

But everyone knows that NSObject is fixed size, right?

-Chris

···

On Oct 3, 2017, at 10:15 PM, Slava Pestov <spestov@apple.com> wrote:

On Oct 3, 2017, at 10:14 PM, Chris Lattner <clattner@nondot.org> wrote:

On Oct 2, 2017, at 11:11 PM, Slava Pestov <spestov@apple.com> wrote:

In any case, even if you’re opposed to these approaches, I’d love for the “alternatives considered” section to indicate what the objection is. I am really very concerned that you’re causing a keyword/attribute explosion and conceptual complexity by adding too many small things to individual parts of the language. We would ideally have a simple and holistic solution to resilience.

I agree with that keyword/attribute explosion is a concern. We also plan on submitting a proposal to add a @fixedContents attribute for structs (currently implemented as @_fixed_layout) which enables more efficient access patterns in resilient code, for example direct access of stored properties, at the cost of preventing new stored properties from being added in a binary-compatible manner. So we would have ‘nonexhaustive’ enums, @fixedContents structs, and @inlinable functions/properties/initializers.

Yes, and then we’ll need something else for classes as well (*head explodes*).

FWIW, I was hoping we wouldn’t need to expose any such attribute for classes (or protocols) at all, because classes are already “slow” and anything we do to make them resilient doesn’t make things much “slower”. But that could change, of course.

The suggestion to have this semantics was originally my fault, I believe, and it arose from the observation that if we have 'inlinable' backed by a symbol in the binary, then we'd also want the 'must be emitted by client' attribute. I think 'must be emitted by client' is going to almost always be preferable for an inlinable function, though, so it's better to have the single attribute with this behavior, only constrained by backward deployment.

-Joe

···

On Oct 3, 2017, at 9:59 PM, Slava Pestov <spestov@apple.com> wrote:

On Oct 3, 2017, at 9:56 PM, Chris Lattner <clattner@nondot.org> wrote:

On Oct 3, 2017, at 9:50 AM, Joe Groff <jgroff@apple.com> wrote:

On Oct 2, 2017, at 10:58 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

We have discussed adding a "versioned @inlinable" variant that preserves the public entry point for older clients, while making the declaration inlinable for newer clients. This will likely be a separate proposal and discussion.

5) It eliminates this complexity.

It wouldn't avoid the complexity, because we want the "non-ABI, always-emit-into-client" behavior for the standard library. For the soon-to-be-ABI-stable libraries where @inlinable even matters, such as the standard library and Apple SDK overlays, there's pretty much perfect overlap between things we want to inline and things we don't want to take up binary space and ABI surface in binaries, so the behavior Slava proposes seems like the right default.

I disagree. The semantics being proposed perfectly overlap with the transitional plan for overlays (which matters for the next few years), but they are the wrong default for anything other than overlays and the wrong thing for long term API evolution over the next 20 years.

Can you elaborate on this? If inlinable functions have public entry points, the version in the framework may or may not be called… because of SIL serialization and inlining. Since the existence of the public entry point doesn’t offer much of a guarantee, it seems desirable to not have the public entry point. For example if the inlinable function is not used elsewhere in the framework, we wouldn’t have to emit it at all. This might make the standard library smaller for instance.

However I’m still waiting for Dave or Jordan to chime in with the original justification for the ‘always emit into client’ behavior. IIRC there was a resilience-related argument too, but I don’t remember what it is now.

These are also ultimately all "power user" features that only a handful of first and second parties shipping binary frameworks need to think about. Apps don't need to care about this at all, and most packages outside the OS can be shipped as source or as fragile binaries, for which these sorts of attributes are unnecessary for the build system to (eventually) provide the desired performance.

-Joe

···

On Oct 4, 2017, at 9:46 AM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Oct 3, 2017, at 10:03 PM, Chris Lattner <clattner@nondot.org> wrote:

On Oct 3, 2017, at 10:04 AM, Joe Groff <jgroff@apple.com> wrote:

On Oct 2, 2017, at 10:58 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

The major question I have is “why yet another attribute”. The thread about exhaustive/extensible enums is similarly proposing introducing another one-off way to be enums fragile, and this is directly related just for function-like things.

I’d love to see rationale in the proposal for why you’re not taking this in one of these directions:

1) Why not another level of access control? There is a reasonable argument that what you’re doing is making something “more public than public” or that you’re making the “body also public”. I’m not strongly in favor of this design approach, but if you agree, the doc should explain why you’re not in favor of it.

2) Why can’t we have a single Swift-wide concept that unifies all of the resilience ideas under a single umbrella like “fragile” - which indicates that the body of a declaration is knowable to clients? There is a very reasonable holistic design where “fragile public func” makes its body inlinable, and “fragile enum” similarly makes the cases knowable to the client (thus making exhaustive switching a possibility). I am strongly in favor of this approach.

I'm of two minds on this. So far, it has seemingly been the case that, for every kind of declaration, there's been exactly one resilience-related attribute, and with blurry enough vision you could say they're all really doing the same thing, "expose this declaration body directly to clients", which only makes sense as a concept for public API. If these properties hold, then sure, it might make sense to consider these all as one "extra-public" concept. On the other hand, we don't know yet whether these properties will hold in the long term as we're actively designing the facilities we need for ABI stability and resilience. From an incremental, iterative design perspective, I think it's wiser to keep them all separate attributes, even if it's a bit messy in the short term. Once the design stabilizes, we can look at whether it makes sense to fold related concepts together. It's easier to fold different things together after the fact then to try to separate things after they've been mixed together.

As I said above, I pretty strongly disagree with this approach. It encourages a “one attribute/declmodifier/keyword per decl kind” approach, which is user hostile. It doesn’t make sense for “exhaustive" to apply to enums, "@inlinable” to apply to function-like things, something else to apply to structs, something else to apply to classes, etc.

Premature abstraction is user-hostile too. These things all have subtly different impacts on language semantics, some of which we're still discovering as we develop their designs.

I disagree. The semantics being proposed perfectly overlap with the transitional plan for overlays (which matters for the next few years), but they are the wrong default for anything other than overlays and the wrong thing for long term API evolution over the next 20 years.

I disagree with this. 'inline' functions in C and C++ have to be backed by a symbol in the binary in order to guarantee function pointer identity, but we don't have that constraint. Without that constraint, there's almost no way that having a fallback definition in the binary is better:

- It becomes an ABI compatibility liability that has to be preserved forever.

This seems like a marginal win at all. Saying that you want to publish a symbol as public API but not have it be ABI is a bit odd. What is the usecase (other than the Swift 3/4/5 transition period)?

- It increases binary size for a function that's rarely used, and which is often much larger as an outlined generic function than the simple operation that can be inlined into client code. Inlining makes the most sense when the inlined operation is smaller than a function call, so in many cases the net dylib + executable size would increase.

I can see this argument, but you’re basically saying that a sufficiently smart programmer can optimize code size based on (near) perfect knowledge of the symbol and all clients. I don’t think this is realistic for a number of reasons. In general, an API vendor has no way to know:

1) how many clients it will have, potentially in multiple modules that get linked into a single app.
2) on which types a generic function will be used with.
3) what the code size tradeoffs ARE, e.g. if you have a large function that doesn’t use the archetype much, there is low bloat.

Furthermore, we have evidence from the C++ community that people are very eager to mark lots of things inlinable regardless of the cost of doing so. Swift may end up being different, but programmers still have no general way to reason about code size given their declaration and without perfect knowledge of the clients.

The code of the approach I’m advocating is one *single* implementation gets generated in the module that defines the decl. This can lead the N instantiations of exactly the same unspecialized code (consider the currying and other cases) in N different modules that end up in an app. This seems like the right tradeoff.

- It increases the uncertainty of the behavior client code sees. If an inlinable function must always be emitted in the client, then client code *always* gets the current definition. If an inlinable function calls into the dylib when the compiler chooses not to inline it, then you may get the current definition, or you may get an older definition from any published version of the dylib. Ideally these all behave the same if the function is inlinable, but quirks are going to be inevitable.

You’re saying that “if an API author incorrectly changes the behavior of their inlinable function” that your approach papers over the bug a little bit better. I don’t see this as something that is important to design around. Not least of which because it will produce other inconsistencies: what if a binary module A is built against the old version of that inlinable function and you app builds against a newer version? Then you have the two inconsistent versions in your app again.

More generally though, an API vendor who does this has broken the fragile/inlinable contract, and they therefore invoked undefined behavior - c'est la vie.

-Chris

···

On Oct 4, 2017, at 9:44 AM, Joe Groff <jgroff@apple.com> wrote:

i’m just tryna follow along here && this is probably a dumb question, but
is it possible for a generic function to be emitted as a set of specialized
functions into the client, but not inlined everywhere? It can be the case
where a large generic function gets slowed down by the large number of
generic operations inside it but it doesn’t make sense for it to be inlined
completely.

···

On Wed, Oct 4, 2017 at 11:44 AM, Joe Groff via swift-evolution < swift-evolution@swift.org> wrote:

On Oct 3, 2017, at 9:56 PM, Chris Lattner <clattner@nondot.org> wrote:

On Oct 3, 2017, at 9:50 AM, Joe Groff <jgroff@apple.com> wrote:

On Oct 2, 2017, at 10:58 PM, Chris Lattner via swift-evolution < > swift-evolution@swift.org> wrote:

We have discussed adding a "versioned @inlinable" variant that preserves
the public entry point for older clients, while making the declaration
inlinable for newer clients. This will likely be a separate proposal and
discussion.

5) It eliminates this complexity.

It wouldn't avoid the complexity, because we want the "non-ABI,
always-emit-into-client" behavior for the standard library. For the
soon-to-be-ABI-stable libraries where @inlinable even matters, such as the
standard library and Apple SDK overlays, there's pretty much perfect
overlap between things we want to inline and things we don't want to take
up binary space and ABI surface in binaries, so the behavior Slava proposes
seems like the right default.

I disagree. The semantics being proposed perfectly overlap with the
transitional plan for overlays (which matters for the next few years), but
they are the wrong default for anything other than overlays and the wrong
thing for long term API evolution over the next 20 years.

I disagree with this. 'inline' functions in C and C++ have to be backed by
a symbol in the binary in order to guarantee function pointer identity, but
we don't have that constraint. Without that constraint, there's almost no
way that having a fallback definition in the binary is better:

- It becomes an ABI compatibility liability that has to be preserved
forever.
- It increases binary size for a function that's rarely used, and which is
often much larger as an outlined generic function than the simple operation
that can be inlined into client code. Inlining makes the most sense when
the inlined operation is smaller than a function call, so in many cases the
net dylib + executable size would increase.
- It increases the uncertainty of the behavior client code sees. If an
inlinable function must always be emitted in the client, then client code
*always* gets the current definition. If an inlinable function calls into
the dylib when the compiler chooses not to inline it, then you may get the
current definition, or you may get an older definition from any published
version of the dylib. Ideally these all behave the same if the function is
inlinable, but quirks are going to be inevitable.

-Joe

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Is this only a problem with fileprivate or does it extend to private members too? I feel like this would be a very valuable feature to support.

Private members too. Consider this example,

struct S {
  private func f() {}
}

The member S.f mangles as _T06struct1SV1f33_AB643CAAAE0894CD0BC8584D7CA3AD23LLyyF. In this case, I suppose we won’t need the private discriminator because there can only be one S.f that’s directly a member of S, and not an extension. However imagine if two different source files both defined extensions of S, with a private member f. You would need to disambiguate them somehow.

The simple-minded way to do this would be to require @_versioned annotations on private and fileprivate members to supply an internally unique alternative name to be used for mangling-as-though-internal (i.e. `@_versioned(my_extension_f)`). Such a function becoming public in an ABI-compatible way would require renaming the "actual" name to the unique @_versioned name.

We have _silgen_name for that, but we really don’t want to expose this more generally because people have been abusing it to make things visible to C, and they should be using @_cdecl instead.

The difference here would be that the "@_versioned name" would be subject to mangling. It's essentially equivalent to a way of specifying a custom discriminator to be hashed so that the source file name is omitted and not ABI. Not that I think it'd be elegant, but it would not be abusable like _silgen_name.

That wouldn’t solve the problem where removing @_versioned(name) and adding public would change the symbol’s name.

However, your idea of mangling versioned private symbols like internal and diagnosing conflicts might be workable.

Slava

···

On Oct 2, 2017, at 9:15 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Mon, Oct 2, 2017 at 22:23 Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

On Oct 2, 2017, at 8:06 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:
On Mon, Oct 2, 2017 at 9:55 PM, Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

On Oct 2, 2017, at 7:52 PM, Kelvin Ma <kelvin13ma@gmail.com <mailto:kelvin13ma@gmail.com>> wrote:

A more elegant refinement could be to have @_versioned private and fileprivate members mangled as though internal, erroring if two or more members with the same name are both @_versioned--would that work?

If you’re going to do that what is the value in having the capability at all?

Solely to have some way of preventing members in one file from calling members in another file at compile time.

again, i should reiterate, most users aren’t compiler engineers and so most
people use access modifiers as a means of code organization. being able to
diagnose when a “private” symbol is being referenced from somewhere it
shouldn’t be is very important; the linking and mangling details should be
handled by the compiler underneath all of that.

···

On Mon, Oct 2, 2017 at 11:45 PM, Slava Pestov <spestov@apple.com> wrote:

On Oct 2, 2017, at 9:15 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Mon, Oct 2, 2017 at 22:23 Slava Pestov <spestov@apple.com> wrote:

On Oct 2, 2017, at 8:06 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Mon, Oct 2, 2017 at 9:55 PM, Slava Pestov <spestov@apple.com> wrote:

On Oct 2, 2017, at 7:52 PM, Kelvin Ma <kelvin13ma@gmail.com> wrote:

Is this only a problem with fileprivate or does it extend to private
members too? I feel like this would be a very valuable feature to support.

Private members too. Consider this example,

struct S {
  private func f() {}
}

The member S.f mangles as _T06struct1SV1f33_
AB643CAAAE0894CD0BC8584D7CA3AD23LLyyF. In this case, I suppose we won’t
need the private discriminator because there can only be one S.f that’s
directly a member of S, and not an extension. However imagine if two
different source files both defined extensions of S, with a private member
f. You would need to disambiguate them somehow.

The simple-minded way to do this would be to require @_versioned
annotations on private and fileprivate members to supply an internally
unique alternative name to be used for mangling-as-though-internal (i.e.
`@_versioned(my_extension_f)`). Such a function becoming public in an
ABI-compatible way would require renaming the "actual" name to the unique
@_versioned name.

We have _silgen_name for that, but we really don’t want to expose this
more generally because people have been abusing it to make things visible
to C, and they should be using @_cdecl instead.

The difference here would be that the "@_versioned name" would be subject
to mangling. It's essentially equivalent to a way of specifying a custom
discriminator to be hashed so that the source file name is omitted and not
ABI. Not that I think it'd be elegant, but it would not be abusable like
_silgen_name.

That wouldn’t solve the problem where removing @_versioned(name) and
adding public would change the symbol’s name.

However, your idea of mangling versioned private symbols like internal and
diagnosing conflicts might be workable.

Slava

A more elegant refinement could be to have @_versioned private and
fileprivate members mangled as though internal, erroring if two or more
members with the same name are both @_versioned--would that work?

If you’re going to do that what is the value in having the capability at
all?

Solely to have some way of preventing members in one file from calling
members in another file at compile time.

I understand your reasoning here, but note that in Jordan’s proposal, he’s adding two new keywords, exhaustive and nonexhaustive. If exhaustive becomes @fragile, does nonexhaustive still make sense?

Slava

···

On Oct 3, 2017, at 10:09 PM, Chris Lattner <clattner@nondot.org> wrote:

On Oct 3, 2017, at 9:59 PM, Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

It wouldn't avoid the complexity, because we want the "non-ABI, always-emit-into-client" behavior for the standard library. For the soon-to-be-ABI-stable libraries where @inlinable even matters, such as the standard library and Apple SDK overlays, there's pretty much perfect overlap between things we want to inline and things we don't want to take up binary space and ABI surface in binaries, so the behavior Slava proposes seems like the right default.

I disagree. The semantics being proposed perfectly overlap with the transitional plan for overlays (which matters for the next few years), but they are the wrong default for anything other than overlays and the wrong thing for long term API evolution over the next 20 years.

Can you elaborate on this? If inlinable functions have public entry points, the version in the framework may or may not be called… because of SIL serialization and inlining. Since the existence of the public entry point doesn’t offer much of a guarantee, it seems desirable to not have the public entry point. For example if the inlinable function is not used elsewhere in the framework, we wouldn’t have to emit it at all. This might make the standard library smaller for instance.

Sure, let me explain the same thing a different way: because the symbol is known to be present, clients are not *forced* to inline the body of the function if it is not profitable. They know that there is always a single canonical implementation of the function, so they aren’t forced to replicate it N times for N clients.

Remember also that there are times where the body of a symbol isn’t the thing that is useful. If you curry a method for example, you want its address. With the proposed inlining semantics, this forces a copy of the body to be emitted in every module that uses it, because you don’t have a canonical name for the thing.

However I’m still waiting for Dave or Jordan to chime in with the original justification for the ‘always emit into client’ behavior. IIRC there was a resilience-related argument too, but I don’t remember what it is now.

The only argument I can imagine is the “If it gets inlined, you’re guaranteed to get the version of the symbol you build against”. The concern is that some instances are inlined and some are not, and if the inline and out of line versions diverge then you can have exciting problems.

My view on that is that you’ve already lost if you’d done this. If you mark a declaration as fragile (allowing it to be inlined) you’ve specifically guaranteed that you’re not going to be changing the observable semantics of the function. Introducing new performance optimizations is fine of course.

In any case, even if you’re opposed to these approaches, I’d love for the “alternatives considered” section to indicate what the objection is. I am really very concerned that you’re causing a keyword/attribute explosion and conceptual complexity by adding too many small things to individual parts of the language. We would ideally have a simple and holistic solution to resilience.

I agree with that keyword/attribute explosion is a concern. We also plan on submitting a proposal to add a @fixedContents attribute for structs (currently implemented as @_fixed_layout) which enables more efficient access patterns in resilient code, for example direct access of stored properties, at the cost of preventing new stored properties from being added in a binary-compatible manner. So we would have ‘nonexhaustive’ enums, @fixedContents structs, and @inlinable functions/properties/initializers.

Yes, and then we’ll need something else for classes as well (*head explodes*).

FWIW, I was hoping we wouldn’t need to expose any such attribute for classes (or protocols) at all, because classes are already “slow” and anything we do to make them resilient doesn’t make things much “slower”. But that could change, of course.

But everyone knows that NSObject is fixed size, right?

Yeah, but the compiler could handle NSObject as a special case. Are there enough other special cases that it is worth documenting and exposing a fragile attribute on classes to the user?

Slava

···

On Oct 3, 2017, at 10:17 PM, Chris Lattner <clattner@nondot.org> wrote:

On Oct 3, 2017, at 10:15 PM, Slava Pestov <spestov@apple.com> wrote:

On Oct 3, 2017, at 10:14 PM, Chris Lattner <clattner@nondot.org> wrote:
On Oct 2, 2017, at 11:11 PM, Slava Pestov <spestov@apple.com> wrote:

-Chris

What is the use case of “must be emitted by client” attribute? If I imagine that the Swift 5 standard library is shipped in the OS, I can see cases where deprecated/legacy shims for Swift3/4 compatibility would be emitted into the client but not shipped in the OS. Those seem relatively obscure though.

The other issue is that if/when the stdlib and overlays start shipping in the OS, that backward deployment will require them to be statically linked into the app (optionally with an arclite style dynamic fallback approach). This problem seems orthogonal to the discussion though.

-Chris

···

On Oct 4, 2017, at 9:36 AM, Joe Groff <jgroff@apple.com> wrote:

It wouldn't avoid the complexity, because we want the "non-ABI, always-emit-into-client" behavior for the standard library. For the soon-to-be-ABI-stable libraries where @inlinable even matters, such as the standard library and Apple SDK overlays, there's pretty much perfect overlap between things we want to inline and things we don't want to take up binary space and ABI surface in binaries, so the behavior Slava proposes seems like the right default.

I disagree. The semantics being proposed perfectly overlap with the transitional plan for overlays (which matters for the next few years), but they are the wrong default for anything other than overlays and the wrong thing for long term API evolution over the next 20 years.

Can you elaborate on this? If inlinable functions have public entry points, the version in the framework may or may not be called… because of SIL serialization and inlining. Since the existence of the public entry point doesn’t offer much of a guarantee, it seems desirable to not have the public entry point. For example if the inlinable function is not used elsewhere in the framework, we wouldn’t have to emit it at all. This might make the standard library smaller for instance.

However I’m still waiting for Dave or Jordan to chime in with the original justification for the ‘always emit into client’ behavior. IIRC there was a resilience-related argument too, but I don’t remember what it is now.

The suggestion to have this semantics was originally my fault, I believe, and it arose from the observation that if we have 'inlinable' backed by a symbol in the binary, then we'd also want the 'must be emitted by client' attribute. I think 'must be emitted by client' is going to almost always be preferable for an inlinable function, though, so it's better to have the single attribute with this behavior, only constrained by backward deployment.

Also, not all inlinable functions are generic. There are perfectly rationale reasons to inline non-generic functions as well, and they don’t benefit from the specialization benefits you’re indicating.

Finally, even if a symbol is generic, it isn’t certain that the client will be able to specialize. The calls may be from unspecialized code after all.

-Chris

···

On Oct 4, 2017, at 9:24 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

- It increases binary size for a function that's rarely used, and which is often much larger as an outlined generic function than the simple operation that can be inlined into client code. Inlining makes the most sense when the inlined operation is smaller than a function call, so in many cases the net dylib + executable size would increase.

I can see this argument, but you’re basically saying that a sufficiently smart programmer can optimize code size based on (near) perfect knowledge of the symbol and all clients. I don’t think this is realistic for a number of reasons. In general, an API vendor has no way to know:

1) how many clients it will have, potentially in multiple modules that get linked into a single app.
2) on which types a generic function will be used with.
3) what the code size tradeoffs ARE, e.g. if you have a large function that doesn’t use the archetype much, there is low bloat.

Furthermore, we have evidence from the C++ community that people are very eager to mark lots of things inlinable regardless of the cost of doing so. Swift may end up being different, but programmers still have no general way to reason about code size given their declaration and without perfect knowledge of the clients.

The code of the approach I’m advocating is one *single* implementation gets generated in the module that defines the decl. This can lead the N instantiations of exactly the same unspecialized code (consider the currying and other cases) in N different modules that end up in an app. This seems like the right tradeoff.

Yep, something like that would definitely be possible by a sufficiently smart compiler, it is compatible with both approaches.

-Chris

···

On Oct 4, 2017, at 9:40 PM, Taylor Swift <kelvin13ma@gmail.com> wrote:

- It becomes an ABI compatibility liability that has to be preserved forever.
- It increases binary size for a function that's rarely used, and which is often much larger as an outlined generic function than the simple operation that can be inlined into client code. Inlining makes the most sense when the inlined operation is smaller than a function call, so in many cases the net dylib + executable size would increase.
- It increases the uncertainty of the behavior client code sees. If an inlinable function must always be emitted in the client, then client code *always* gets the current definition. If an inlinable function calls into the dylib when the compiler chooses not to inline it, then you may get the current definition, or you may get an older definition from any published version of the dylib. Ideally these all behave the same if the function is inlinable, but quirks are going to be inevitable.

i’m just tryna follow along here && this is probably a dumb question, but is it possible for a generic function to be emitted as a set of specialized functions into the client, but not inlined everywhere? It can be the case where a large generic function gets slowed down by the large number of generic operations inside it but it doesn’t make sense for it to be inlined completely.

This is already possible. The optimizer doesn’t have to inline an @_inlineable function at its call site; it can emit a call to a specialized version instead.

Slava

···

On Oct 4, 2017, at 9:40 PM, Taylor Swift via swift-evolution <swift-evolution@swift.org> wrote:

i’m just tryna follow along here && this is probably a dumb question, but is it possible for a generic function to be emitted as a set of specialized functions into the client, but not inlined everywhere? It can be the case where a large generic function gets slowed down by the large number of generic operations inside it but it doesn’t make sense for it to be inlined completely.

Is there a reason using @_specialize() and @_inlineable together is slower than using @_inlineable by itself?

···

On Oct 5, 2017, at 12:52 AM, Slava Pestov <spestov@apple.com> wrote:

On Oct 4, 2017, at 9:40 PM, Taylor Swift via swift-evolution <swift-evolution@swift.org> wrote:

i’m just tryna follow along here && this is probably a dumb question, but is it possible for a generic function to be emitted as a set of specialized functions into the client, but not inlined everywhere? It can be the case where a large generic function gets slowed down by the large number of generic operations inside it but it doesn’t make sense for it to be inlined completely.

This is already possible. The optimizer doesn’t have to inline an @_inlineable function at its call site; it can emit a call to a specialized version instead.

Slava

I disagree. The semantics being proposed perfectly overlap with the transitional plan for overlays (which matters for the next few years), but they are the wrong default for anything other than overlays and the wrong thing for long term API evolution over the next 20 years.

I disagree with this. 'inline' functions in C and C++ have to be backed by a symbol in the binary in order to guarantee function pointer identity, but we don't have that constraint. Without that constraint, there's almost no way that having a fallback definition in the binary is better:

- It becomes an ABI compatibility liability that has to be preserved forever.

This seems like a marginal win at all. Saying that you want to publish a symbol as public API but not have it be ABI is a bit odd. What is the usecase (other than the Swift 3/4/5 transition period)?

I think it's a bigger win than you give it credit. If the function only exists in client code, then the library can much more aggressively deprecate and remove or replace the API, since it only has to worry about source compatibility and not deployed binary compatibility. If introducing `@inlinable` later further requires new clients to emit-into-client from that point on, so that the in-dylib entry point only exists for backward compatibility, then you get a free "linked-on-or-after" boundary where you can fix quirks or shed compatibility behavior in the inlinable version while preserving it in the binary.

- It increases binary size for a function that's rarely used, and which is often much larger as an outlined generic function than the simple operation that can be inlined into client code. Inlining makes the most sense when the inlined operation is smaller than a function call, so in many cases the net dylib + executable size would increase.

I can see this argument, but you’re basically saying that a sufficiently smart programmer can optimize code size based on (near) perfect knowledge of the symbol and all clients. I don’t think this is realistic for a number of reasons. In general, an API vendor has no way to know:

1) how many clients it will have, potentially in multiple modules that get linked into a single app.
2) on which types a generic function will be used with.
3) what the code size tradeoffs ARE, e.g. if you have a large function that doesn’t use the archetype much, there is low bloat.

Furthermore, we have evidence from the C++ community that people are very eager to mark lots of things inlinable regardless of the cost of doing so. Swift may end up being different, but programmers still have no general way to reason about code size given their declaration and without perfect knowledge of the clients.

The code of the approach I’m advocating is one *single* implementation gets generated in the module that defines the decl. This can lead the N instantiations of exactly the same unspecialized code (consider the currying and other cases) in N different modules that end up in an app. This seems like the right tradeoff.

If we're talking about inlinable functions, then we're already talking about functions generally on the small end of the scale, so duplication is less of an issue. Furthermore, the duplication hazard only exists at dylib boundaries, since among static libraries we can still instantiate all the different instantiations of the function as ODR and let the linker fold them. For most apps there aren't going to be that many of those boundaries.

- It increases the uncertainty of the behavior client code sees. If an inlinable function must always be emitted in the client, then client code *always* gets the current definition. If an inlinable function calls into the dylib when the compiler chooses not to inline it, then you may get the current definition, or you may get an older definition from any published version of the dylib. Ideally these all behave the same if the function is inlinable, but quirks are going to be inevitable.

You’re saying that “if an API author incorrectly changes the behavior of their inlinable function” that your approach papers over the bug a little bit better. I don’t see this as something that is important to design around. Not least of which because it will produce other inconsistencies: what if a binary module A is built against the old version of that inlinable function and you app builds against a newer version? Then you have the two inconsistent versions in your app again.

More generally though, an API vendor who does this has broken the fragile/inlinable contract, and they therefore invoked undefined behavior - c'est la vie.

The contract doesn't need to be nearly as strict with emitted-into-client code, though, since you can make fixes or behavior changes that only take hold when you recompile with a newer version of the library, linked-on-or-after style.

-Joe

···

On Oct 4, 2017, at 9:24 PM, Chris Lattner <clattner@nondot.org> wrote:
On Oct 4, 2017, at 9:44 AM, Joe Groff <jgroff@apple.com> wrote:

Independently of how exhaustive is spelled, nonexhaustive doesn’t make sense to me. It should be the default. Swift doesn’t have keywords to redundantly specify the default unless there is a specific reason to be able to do that.

The example often cited is “nonmutating”, but it isn’t there to cover the default: it is specifically required because setters default to mutating so it must exist to be change that default.

-Chris

···

On Oct 3, 2017, at 10:11 PM, Slava Pestov <spestov@apple.com> wrote:

However I’m still waiting for Dave or Jordan to chime in with the original justification for the ‘always emit into client’ behavior. IIRC there was a resilience-related argument too, but I don’t remember what it is now.

The only argument I can imagine is the “If it gets inlined, you’re guaranteed to get the version of the symbol you build against”. The concern is that some instances are inlined and some are not, and if the inline and out of line versions diverge then you can have exciting problems.

My view on that is that you’ve already lost if you’d done this. If you mark a declaration as fragile (allowing it to be inlined) you’ve specifically guaranteed that you’re not going to be changing the observable semantics of the function. Introducing new performance optimizations is fine of course.

I understand your reasoning here, but note that in Jordan’s proposal, he’s adding two new keywords, exhaustive and nonexhaustive. If exhaustive becomes @fragile, does nonexhaustive still make sense?