泛型和协议类型函数参数之间的实际区别是什么? [英] What is the in-practice difference between generic and protocol-typed function parameters?

查看:32
本文介绍了泛型和协议类型函数参数之间的实际区别是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

给定一个没有任何关联类型的协议:

protocol SomeProtocol{var someProperty: Int { get }}

这两个函数在实践中的区别是什么(意思不是一个是通用的,另一个不是")?它们是否生成不同的代码,它们是否具有不同的运行时特征?当协议或功能变得重要时,这些差异会改变吗?(因为编译器可能会内联这样的东西)

func generic(some: T) ->整数{返回 some.someProperty}func nonGeneric(some: SomeProtocol) ->整数{返回 some.someProperty}

我主要询问编译器功能的差异,我了解两者的语言级别含义.基本上,nonGeneric 是否意味着代码大小恒定但动态调度速度较慢,与 generic 相比,每种传递的类型使用不断增长的代码大小,但静态调度速度较快?

解决方案

(我意识到 OP 对语言影响的要求较少,而对编译器的功能要求更多——但我觉得列出泛型和协议类型函数参数之间的一般区别)

1.受协议约束的通用占位符必须满足具体类型

这是协议不符合自身的结果,因此您不能调用generic(some:) 带有 SomeProtocol 类型参数.

struct Foo : SomeProtocol {var someProperty: Int}//当然这里的解决方案是去掉多余的'SomeProtocol'类型注解//并让 foo 为 Foo 类型,但此问题适用于任何//'任何符合 SomeProtocol' 类型变量的东西都是必需的.让 foo : SomeProtocol = Foo(someProperty: 42)generic(some: something)//编译器错误:无法使用参数列表调用generic"//'(some: SomeProtocol)' 类型

这是因为泛型函数需要一个符合 SomeProtocolT 类型的参数——但是 SomeProtocolnot 符合 SomeProtocol 的类型.

然而,一个非泛型函数,参数类型为 SomeProtocol接受 foo 作为参数:

nonGeneric(some: foo)//编译良好

这是因为它接受任何可以输入为 SomeProtocol"的东西,而不是符合 SomeProtocol 的特定类型".

2.专业化

这个精彩的 WWDC 演讲所述,存在容器"用于表示协议类型的值.

这个容器包括:

  • 一个用于存储值本身的值缓冲区,长度为 3 个字.大于此值的值将被分配到堆中,并且对该值的引用将存储在值缓冲区中(因为引用的大小仅为 1 个字).

  • 指向类型元数据的指针.类型的元数据中包含一个指向其值见证表的指针,该表管理存在容器中值的生命周期.

  • 一个或(在协议组合) 多个指向给定类型的协议见证表的指针.这些表跟踪可用于调用给定协议类型实例的协议要求的类型实现.

默认情况下,使用类似的结构将值传递给通用占位符类型的参数.

  • 参数存储在一个 3 字的值缓冲区中(可以堆分配),然后传递给参数.

  • 对于每个通用占位符,该函数采用元数据指针参数.用于满足占位符的类型的元类型在调用时传递给此参数.

  • 对于给定占位符上的每个协议约束,该函数采用协议见证表指针参数.

然而,在优化的构建中,Swift 能够专门化泛型函数的实现——允许编译器为它所应用的每种类型的泛型占位符生成一个新函数.这允许参数总是简单地按值传递,以增加代码大小为代价.然而,正如接下来所说的那样,积极的编译器优化,尤其是内联,可以抵消这种膨胀.

3.发送协议要求

因为泛型函数能够被特化,所以对传入的泛型参数的方法调用能够被静态调度(尽管显然不适用于使用动态多态的类型,例如非 final 类).

然而,协议类型的函数通常不能从中受益,因为它们不能从专业化中受益.因此,对协议类型参数的方法调用将通过该给定参数的协议见证表动态调度,这会更昂贵.

尽管如此,简单的协议类型函数可以从内联中受益.在这种情况下,编译器 能够消除值缓冲区和协议以及值见证表的开销(这可以通过检查 -O 构建中发出的 SIL 看到),允许它静态地以与泛型函数相同的方式分发方法.但是,与泛型专业化不同,对于给定的函数不能保证这种优化(除非您 应用@inline(__always) 属性——但通常最好让编译器决定这一点.

因此,在性能方面,泛型函数比协议类型函数更受青睐,因为它们可以实现方法的静态分派,而无需内联.

4.过载解析

在执行重载解析时,编译器会偏爱协议类型的函数而不是泛型函数.

struct Foo : SomeProtocol {var someProperty: Int}func bar(_ some: T) {打印(通用")}func bar(_ some: SomeProtocol) {打印(协议类型")}bar(Foo(someProperty: 5))//协议类型

这是因为 Swift 更喜欢显式类型的参数而不是泛型参数(请参阅此问答).

5.通用占位符强制执行相同的类型

如前所述,使用通用占位符允许您强制使用相同的类型用于使用该特定占位符键入的所有参数/返回.

功能:

func generic(a: T, b: T) ->T{返回 a.someProperty <b.someProperty ?乙:一}

接受两个参数并返回相同具体类型,该类型符合SomeProtocol.

然而功能:

func nongeneric(a: SomeProtocol, b: SomeProtocol) ->一些协议{返回 a.someProperty <b.someProperty ?乙:一}

除了参数之外不携带任何承诺,并且返回必须符合SomeProtocol.实际传递和返回的具体类型不一定相同.

Given a protocol without any associated types:

protocol SomeProtocol
{
    var someProperty: Int { get }
}

What is the difference between these two functions, in practice (meaning not "one is generic and the other is not")? Do they generate different code, do they have different runtime characteristics? Do these differences change when the protocol or functions become non-trivial? (since a compiler could probably inline something like this)

func generic<T: SomeProtocol>(some: T) -> Int
{
    return some.someProperty
}

func nonGeneric(some: SomeProtocol) -> Int
{
    return some.someProperty
}

I'm mostly asking about differences in what the compiler does, I understand the language-level implications of both. Basically, does nonGeneric imply a constant code size but slower dynamic dispatch, vs. generic using a growing code size per type passed, but with fast static dispatch?

解决方案

(I realise that OP is asking less about the language implications and more about what the compiler does – but I feel it's also worthwhile also to list the general differences between generic and protocol-typed function parameters)

1. A generic placeholder constrained by a protocol must be satisfied with a concrete type

This is a consequence of protocols not conforming to themselves, therefore you cannot call generic(some:) with a SomeProtocol typed argument.

struct Foo : SomeProtocol {
    var someProperty: Int
}

// of course the solution here is to remove the redundant 'SomeProtocol' type annotation
// and let foo be of type Foo, but this problem is applicable anywhere an
// 'anything that conforms to SomeProtocol' typed variable is required.
let foo : SomeProtocol = Foo(someProperty: 42)

generic(some: something) // compiler error: cannot invoke 'generic' with an argument list
                         // of type '(some: SomeProtocol)'

This is because the generic function expects an argument of some type T that conforms to SomeProtocol – but SomeProtocol is not a type that conforms to SomeProtocol.

A non-generic function however, with a parameter type of SomeProtocol, will accept foo as an argument:

nonGeneric(some: foo) // compiles fine

This is because it accepts 'anything that can be typed as a SomeProtocol', rather than 'a specific type that conforms to SomeProtocol'.

2. Specialisation

As covered in this fantastic WWDC talk, an 'existential container' is used in order to represent a protocol-typed value.

This container consists of:

  • A value buffer to store the value itself, which is 3 words in length. Values larger than this will be heap allocated, and a reference to the value will be stored in the value buffer (as a reference is just 1 word in size).

  • A pointer to the type's metadata. Included in the type's metadata is a pointer to its value witness table, which manages the lifetime of value in the existential container.

  • One or (in the case of protocol composition) multiple pointers to protocol witness tables for the given type. These tables keep track of the type's implementation of the protocol requirements available to call on the given protocol-typed instance.

By default, a similar structure is used in order to pass a value into a generic placeholder typed argument.

  • The argument is stored in a 3 word value buffer (which may heap allocate), which is then passed to the parameter.

  • For each generic placeholder, the function takes a metadata pointer parameter. The metatype of the type that's used to satisfy the placeholder is passed to this parameter when calling.

  • For each protocol constraint on a given placeholder, the function takes a protocol witness table pointer parameter.

However, in optimised builds, Swift is able to specialise the implementations of generic functions – allowing the compiler to generate a new function for each type of generic placeholder that it's applied with. This allows for arguments to always be simply passed by value, at the cost of increasing code size. However, as the talk then goes onto say, aggressive compiler optimisations, particularly inlining, can counteract this bloat.

3. Dispatch of protocol requirements

Because of the fact that generic functions are able to be specialised, method calls on generic arguments passed in are able to be statically dispatched (although obviously not for types that use dynamic polymorphism, such as non-final classes).

Protocol-typed functions however generally cannot benefit from this, as they don't benefit from specialisation. Therefore method calls on a protocol-typed argument will be dynamically dispatched via the protocol witness table for that given argument, which is more expensive.

Although that being said, simple protocol-typed functions may be able to benefit from inlining. In such cases, the compiler is able to eliminate the overhead of the value buffer and protocol and value witness tables (this can be seen by examining the SIL emitted in a -O build), allowing it to statically dispatch methods in the same way as generic functions. However, unlike generic specialisation, this optimisation is not guaranteed for a given function (unless you apply the @inline(__always) attribute – but usually it's best to let the compiler decide this).

Therefore in general, generic functions are favoured over protocol-typed functions in terms of performance, as they can achieve static dispatch of methods without having to be inlined.

4. Overload resolution

When performing overload resolution, the compiler will favour the protocol-typed function over the generic one.

struct Foo : SomeProtocol {
    var someProperty: Int
}

func bar<T : SomeProtocol>(_ some: T) {
    print("generic")
}

func bar(_ some: SomeProtocol) {
    print("protocol-typed")
}

bar(Foo(someProperty: 5)) // protocol-typed

This is because Swift favours an explicitly typed parameter over a generic one (see this Q&A).

5. Generic placeholders enforce the same type

As already said, using a generic placeholder allows you to enforce that the same type is used for all parameters/returns that are typed with that particular placeholder.

The function:

func generic<T : SomeProtocol>(a: T, b: T) -> T {
    return a.someProperty < b.someProperty ? b : a
}

takes two arguments and has a return of the same concrete type, where that type conforms to SomeProtocol.

However the function:

func nongeneric(a: SomeProtocol, b: SomeProtocol) -> SomeProtocol {
    return a.someProperty < b.someProperty ? b : a
}

carries no promises other than the arguments and return must conform to SomeProtocol. The actual concrete types that are passed and returned do not necessarily have to be the same.

这篇关于泛型和协议类型函数参数之间的实际区别是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆