Certainly, if the compiler can see the type passed to myDump
at the point of the call.
Many of these questions can be easily answered just by compiling programs; I use this command to dump readable assembly for a single function or method that I call testMe
:
swiftc -O -S sourcefile.swift \
| sed -e '/testMe/,/retq/!d;/retq/q' \
| xcrun swift-demangle
(The second line strips everything between the first mention of testMe
and its return instruction. The third line demangles names to make things readable)
Interestingly, Swift's optimizer forgets important type information when faced with an Any
, as you can see by running it on this program (I removed printing and string interpolation for simplicity):
protocol Dumpable {
func dump() -> String
}
extension Int: Dumpable {
@inlinable
func dump() -> String { "int" }
}
extension String: Dumpable {
func dump() -> String { "string" }
}
func myDump(_ value: Any) -> String {
return !(value is Dumpable) ? "any" : (value as! Dumpable).dump()
}
func testMe() -> String {
return myDump(3)
}
In the result you can see the creation of boxed existentials and calls to the Swift runtime's dynamic casting machinery.
Assembly output
.private_extern x.testMe() -> Swift.String
.globl x.testMe() -> Swift.String
.p2align 4, 0x90
x.testMe() -> Swift.String:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
pushq %r15
pushq %r14
pushq %r13
pushq %r12
pushq %rbx
subq $104, %rsp
.cfi_offset %rbx, -56
.cfi_offset %r12, -48
.cfi_offset %r13, -40
.cfi_offset %r14, -32
.cfi_offset %r15, -24
movq type metadata for Swift.Int@GOTPCREL(%rip), %rax
movq %rax, -48(%rbp)
movq $3, -72(%rbp)
leaq -144(%rbp), %r15
leaq -72(%rbp), %rdi
movq %r15, %rsi
callq outlined init with copy of Any
leaq -112(%rbp), %r12
leaq demangling cache variable for type metadata for x.Dumpable(%rip), %rdi
callq ___swift_instantiateConcreteTypeFromMangledName
movq %rax, %r14
movq type metadata for Any@GOTPCREL(%rip), %rbx
addq $8, %rbx
movl $6, %r8d
movq %r12, %rdi
movq %r15, %rsi
movq %rbx, %rdx
movq %rax, %rcx
callq _swift_dynamicCast
testb %al, %al
je LBB10_1
leaq -112(%rbp), %rdi
callq ___swift_destroy_boxed_opaque_existential_0
leaq -144(%rbp), %r15
leaq -72(%rbp), %rdi
movq %r15, %rsi
callq outlined init with copy of Any
leaq -112(%rbp), %r12
movl $7, %r8d
movq %r12, %rdi
movq %r15, %rsi
movq %rbx, %rdx
movq %r14, %rcx
callq _swift_dynamicCast
movq -88(%rbp), %rbx
movq -80(%rbp), %r14
movq %r12, %rdi
movq %rbx, %rsi
callq ___swift_project_boxed_opaque_existential_1
movq %rax, %r13
movq %rbx, %rdi
movq %r14, %rsi
callq *8(%r14)
movq %rax, %rbx
movq %rdx, %r14
movq %r12, %rdi
callq ___swift_destroy_boxed_opaque_existential_0
jmp LBB10_3
LBB10_1:
movabsq $-2089670227099910144, %r14
movl $7958113, %ebx
LBB10_3:
leaq -72(%rbp), %rdi
callq ___swift_destroy_boxed_opaque_existential_0
movq %rbx, %rax
movq %r14, %rdx
addq $104, %rsp
popq %rbx
popq %r12
popq %r13
popq %r14
popq %r15
popq %rbp
retq
However, you only need to write myDump
as a generic to get the performance back:
func myDump<T>(_ value: T) -> String {
return !(value is Dumpable) ? "any" : (value as! Dumpable).dump()
}
which results in:
.private_extern x.testMe() -> Swift.String
.globl x.testMe() -> Swift.String
.p2align 4, 0x90
x.testMe() -> Swift.String:
pushq %rbp
movq %rsp, %rbp
movabsq $-2089670227099910144, %rdx
movl $7630441, %eax
popq %rbp
retq
Since the generic formulation and the Any
formulation are semantically equivalent, there's no reason the optimizer couldn't have made the former as efficient as the latter; it just didn't.