-
Notifications
You must be signed in to change notification settings - Fork 0
Avoiding generic programming pitfalls
Excessive generic function specialization can lead to large executables, and added instruction cache pressure can eliminate any performance benefit. By default, a Clay function with no type information will specialize for every set of input types it's given:
muladd(a, b, c) = a * b + c; // A separate muladd function will be compiled for every set of input types
For small functions that will likely be inlined, the cost will be minimal, but for a large function, this is problematic. An obvious way to curtail the number of instances is to explicitly limit the set of allowed types:
// Only allow int inputs
muladd(a: Int, b: Int, c: Int) = a * b + c;
// Only allow Int or Float inputs
[A, B, C | allValues?(T => inValues?(T, Int, Float), A, B, C)]
muladd(a: A, b: B, c: C) = a * b + c;
Limiting to a small set of types may be undesirable, especially when dealing with various different-sized integer or floating-point types. If a function operates on a related group of types, it can convert those types to a canonical "funnel" type:
// Generic wrapper accepts any set of Integer? types and funnels them down to Int
[A, B, C | allValues?(Integer?, A, B, C)]
muladd(a: A, b: B, c: C) = muladd(Int(a), Int(b), Int(c));
// Principal overload implements the actual operation
overload muladd(a: Int, b: Int, c: Int) = a * b + c;
The size of the generic stub will then be minimal—only the size of the conversions and the call to the principal overload—while only one instance of the actual body of the function needs to be generated.