Compiler Directive for Typechecks

Hello everyone,

Sometimes, you write code like this:

struct Foo {
   func doSomething<T>(_ object: T) {
      print("generic")
   }
   func doSomething(_ object: String) {
     print("specialized")
   }
}

If you call doSomething on a Foo with a String, the specialized method takes precedence - as you would expect. This can be beneficial for performance (think: transposing a matrix using the BLAS whenever the entries are Float or Double).

However, if the generic doSomething is a protocol method, Foo inherits from that protocol and you declare a variable where the static type is that protocol ...

protocol Bar {
   func doSomething<T>(_ object: T)
}

let bar : Bar = Foo()

then, calling doSomething with a String will print generic (see https://stackoverflow.com/questions/55148285/protocol-with-generics-and-specialization).

A simple workaround is to insert a type-check into the method:

func doSomething<T>(_ object: T) {
   if object is String {
      print("specialized")
   }
   else {
      print("generic")
   }
}

However, this is a dynamic typecheck and will be executed only at runtime. If you do the specialization to achieve a performance benefit, this benefit may actually be considerably diminished by this runtime type-check (at least if you try to do something very high performance).

A nice-to-have feature would therefore be a compiler directive for type checks:

func doSomething<T>(_ object: T) {
   #if equal(T, String)
   print("specialized")
   #else
   print("generic")  
   #endif
}

Is there something like this already? If no, what do y'all think?

I think that if the goal is high performance and the functions do the same thing, the swift compiler already have mechanism in place to specialize generic code in order to achieve better performance. As for example:

func increment<T: FloatingPoint >(_ value: T) -> T {
    return value + 1
}
let d: Double = 1
increment(d)

Depending of the optimization mode we are compiling in, if we compiling in -O (Optimized for Speed) the Optimizer will generate a specialized version of the generic function for double. If you are compiling for with Optimized for Size, I think this not going to happen because generic specialization increases binary size (as it generates more specialized code).
We can also hint the compiler to specialize code, using @_specialize, but since is an underscore attribute usage may not be recommended.

@_specialize(exported: true, where T==Double)
func increment<T: FloatingPoint >(_ value: T) -> T {
    return value + 1
}

Note: because generic specialization is not a guaranteed optimization pass, meaning we don’t have in -Onone, so to benchmark and experiment with generic specialization we have to compile using -O.

I'm not really sure is that what solves your problem, but is a great way that the compiler allows us to write generic code without lose performance.

1 Like

Try

if T.self == String.self {
  ...
}

That's what I use if I want compile-time checking where doSomething is inlined.

3 Likes

Ah, thanks! I didn't know the compiler is smart enough to optimize this runtime check away, but it makes sense.

The Swift compiler is quite smart when optimizing, like folding closures, eliminating runtime type-checking on specialized/inlined functions, etc.

The trickiest part is ensuring that the function call uses a specialized version or that it is inlined. Usually, I use @inline(__always)* when optimizing to gauge the potential speed I could achieve, but I'd remove that annotation thereafter.

* You may also need to mark the function as @inlinable if you're crossing module boundaries. Not too sure about that, though.

Terms of Service

Privacy Policy

Cookie Policy