Automatic generalization and performance F #

Recently, I came across unexpected code optimization and wanted to verify that my interpretation of what I was observing was correct. The following is a simplified example of the situation:

let demo = let swap fst snd i = if i = fst then snd else if i = snd then fst else i [ for i in 1 .. 10000 -> swap 1 ii ] let demo2 = let swap (fst: int) snd i = if i = fst then snd else if i = snd then fst else i [ for i in 1 .. 10000 -> swap 1 ii ] 

The only difference between the two blocks of code is that in the second case, I explicitly declare the swap arguments as integers. However, when I run 2 fragments in fsi C # time, I get:

Case 1 Real: 00: 00: 00.011, CPU: 00: 00: 00.000, GC gen0: 0, gen1: 0, gen2: 0
Case 2: 00: 00: 00.004, CPU: 00: 00: 00.015, GC gen0: 0, gen1: 0, gen2: 0

i.e. the second snapshot starts 3 times faster than the first. The absolute difference in performance here is obviously not a problem, but if I used the swap function many times, it would accumulate.

My assumption is that the reason for the performance hit is that in the first case, swap is generic and โ€œrequires equalityโ€, and checks to see if it supports int, and the second not to check. Is this the reason this is happening, or am I missing something else? And more generally, should automatic generalization be considered a double-edged sword, that is, an amazing feature that can have unexpected performance implications?

+6
source share
2 answers

I think this is usually the same case as in the question Why is this F # code so slow . In this question, the performance issue is caused by a constraint requiring comparison , and in your case it is caused by an equality constraint.

In both cases, the compiled common code must use interfaces (and boxing), while specialized compiled code can directly use IL instructions to compare or equal integers or floating point numbers.

Two ways to avoid performance issues:

  • Specialize code for using int or float , as you did
  • Mark the function as inline so that the compiler automatically specializes it.

For smaller functions, the second approach is better because it does not generate too much code, and you can still write functions in a general way. If you use the function only for a single type (by design), then it is probably advisable to use the first approach.

+10
source

The reason for the difference is that the compiler generates calls

  IL_0002: call bool class [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives/HashCompare::GenericEqualityIntrinsic<!!0> (!!0, !!0) 

in the general version, while the int version can just compare directly.

If you used inline , I suspect that this problem will disappear as the compiler now has additional type information

+3
source

Source: https://habr.com/ru/post/915048/


All Articles