F# compiler throws OutOfMemoryException
Asked Answered
I

1

12

The project I use contains a lot of classes inherited from single base class. In unit tests I need to compare received results by types and data.

When I use match comparison by types in the case when conditions list contains about enough many different conditions compiler throws OutOfMemoryException.

For example, following F# code raises System.OutOfMemoryException (parameter error FS0193) during compilation (and compilation took about 30 seconds before exception was thrown)

type IBaseClass() = class end

type IChildClass1 () = inherit IBaseClass () 

type IChildClass2 () = inherit IBaseClass () 

type IChildClass3 () = inherit IBaseClass () 

type IChildClass4 () = inherit IBaseClass () 

type IChildClass5 () = inherit IBaseClass () 

type IChildClass6 () = inherit IBaseClass () 

type IChildClass7 () = inherit IBaseClass () 

type IChildClass8 () = inherit IBaseClass () 

type IChildClass9 () = inherit IBaseClass () 

type IChildClass10 () = inherit IBaseClass () 

type IChildClass11 () = inherit IBaseClass () 

type IChildClass12 () = inherit IBaseClass () 

type IChildClass13 () = inherit IBaseClass () 

type IChildClass14 () = inherit IBaseClass () 

type IChildClass15 () = inherit IBaseClass () 

type IChildClass16 () = inherit IBaseClass () 

type IChildClass17 () = inherit IBaseClass () 

type IChildClass18 () = inherit IBaseClass () 

type IChildClass19 () = inherit IBaseClass () 

type IChildClass20 () = inherit IBaseClass () 


let AreEqual (original: IBaseClass) (compareWith: IBaseClass) : bool =
    match (original, compareWith) with
    | (:? IChildClass1 as a), (:? IChildClass1 as b) -> a = b
    | (:? IChildClass2 as a), (:? IChildClass2 as b) -> a = b
    | (:? IChildClass3 as a), (:? IChildClass3 as b) -> a = b
    | (:? IChildClass4 as a), (:? IChildClass4 as b) -> a = b
    | (:? IChildClass5 as a), (:? IChildClass5 as b) -> a = b
    | (:? IChildClass6 as a), (:? IChildClass6 as b) -> a = b
    | (:? IChildClass7 as a), (:? IChildClass7 as b) -> a = b
    | (:? IChildClass8 as a), (:? IChildClass8 as b) -> a = b
    | (:? IChildClass9 as a), (:? IChildClass9 as b) -> a = b
    | (:? IChildClass10 as a), (:? IChildClass10 as b) -> a = b
    | (:? IChildClass11 as a), (:? IChildClass11 as b) -> a = b
    | (:? IChildClass12 as a), (:? IChildClass12 as b) -> a = b
    | (:? IChildClass13 as a), (:? IChildClass13 as b) -> a = b
    | (:? IChildClass14 as a), (:? IChildClass14 as b) -> a = b
    | (:? IChildClass15 as a), (:? IChildClass15 as b) -> a = b
    | (:? IChildClass16 as a), (:? IChildClass16 as b) -> a = b
    | (:? IChildClass17 as a), (:? IChildClass17 as b) -> a = b
    | (:? IChildClass18 as a), (:? IChildClass18 as b) -> a = b
    | (:? IChildClass19 as a), (:? IChildClass19 as b) -> a = b
    | (:? IChildClass20 as a), (:? IChildClass20 as b) -> a = b
    | _ -> false

For sure I can add IEquatable interface to my IBaseClass class that's will avoid usage of such match construction, or add int Kind member (or enum) to the IBaseClass interface and make match not by types, but by some int value.

Note, I've tried to compile the same project in MS VS 2010 and MSVS 11 Beta, and had the same compiler error

Question: Why the compiler's OutOfMemoryException happens in my case (is it known compiler bug or other limitation), how should I reorganize my match condition to avoid it?

Update When I put classes into discriminated unions and use similar match comparison Fsc.exe compiles the project without exception

type AllClasses = 
    | ChildClass1 of IChildClass1 | ChildClass2 of IChildClass2 | ChildClass3 of IChildClass3 | ChildClass4 of IChildClass4 | ChildClass5 of IChildClass5 | ChildClass6 of IChildClass6
    | ChildClass7 of IChildClass7 | ChildClass8 of IChildClass8 | ChildClass9 of IChildClass9 | ChildClass10 of IChildClass10 | ChildClass11 of IChildClass11 | ChildClass12 of IChildClass12
    | ChildClass13 of IChildClass13 | ChildClass14 of IChildClass14 | ChildClass15 of IChildClass15 | ChildClass16 of IChildClass16 | ChildClass17 of IChildClass17 | ChildClass18 of IChildClass18 
    | ChildClass19 of IChildClass19 | ChildClass20 of IChildClass20

let AreEqual2 (original: AllClasses) (compareWith: AllClasses) : bool =
    match (original, compareWith) with
    | ChildClass1(a), ChildClass1(b) -> a = b
    | ChildClass2(a), ChildClass2(b) -> a = b
    | ChildClass3(a), ChildClass3(b) -> a = b
    | ChildClass4(a), ChildClass4(b) -> a = b
    | ChildClass5(a), ChildClass5(b) -> a = b
    | ChildClass6(a), ChildClass6(b) -> a = b
    | ChildClass7(a), ChildClass7(b) -> a = b
    | ChildClass8(a), ChildClass8(b) -> a = b
    | ChildClass9(a), ChildClass9(b) -> a = b
    | ChildClass10(a), ChildClass10(b) -> a = b
    | ChildClass11(a), ChildClass11(b) -> a = b
    | ChildClass12(a), ChildClass12(b) -> a = b
    | ChildClass13(a), ChildClass13(b) -> a = b
    | ChildClass14(a), ChildClass14(b) -> a = b
    | ChildClass15(a), ChildClass15(b) -> a = b
    | ChildClass16(a), ChildClass16(b) -> a = b
    | ChildClass17(a), ChildClass17(b) -> a = b
    | ChildClass18(a), ChildClass18(b) -> a = b
    | ChildClass19(a), ChildClass19(b) -> a = b
    | ChildClass20(a), ChildClass20(b) -> a = b
    | _ -> false

Thanks

Interpellation answered 16/5, 2012 at 11:13 Comment(2)
I assume you are using IChildClass1-20 only for illustration purposes, or is this actual code? I don't think the compiler itself should ever throw an OutOfMemoryException (but I don't have the answer)Intravasation
@Intravasation - yes, the class names are only for illustration purpose, classes for sure have different names. Note on my machine if I use IChildClass1-18 in AreEqual - project is compilable, 19 and more - boom... exceptionInterpellation
T
9

This is caused by how the F# compiler compiles pattern matching on tuples in this case. I'm not entirely sure when exactly you run in this particular problem and when the compiler uses other approach, but here is an explanation why it fails in this case...

If you write a pattern matching like the one in your example, the compiler essentially generates a decision tree that tests the first pattern for original (:? IChildClass1) and then generates two branches. The first branch checks whether compareWith is also IChildClass1. If yes, it runs the first case. The rest of the pattern matching is then duplicated in both of the branches, so you get something like (you can check this by looking at the compiled code for smaller number of cases using ILSpy):

if (original is IChildClass1)
  if (compareWith is IChildClass1)
    case #1
  if (original is IChildClass2)
    if (compareWith is IChildClass2)
      case #2
    if (original is IChildClass3)
      (...)
else
  if (original is IChildClass2)
    if (compareWith is IChildClass2)
      case #2
    if (original is IChildClass3)
      (...)

This means that the size of the generated code is exponentially proportional to the number of cases in this pattern matching. For 20 of cases, the compiler tries to create 2^20 branches, which (unsurprisingly) fails.

Tuchman answered 16/5, 2012 at 12:29 Comment(5)
thank you for the explanation. yes, I understand that Fsc creates 2^20 branches, what causes the exception. But if I put interfaces into discriminated unions (type AllClasses = ChildClass1 of IChildClass1 | ChildClass2 of IChildClass2 | ChildClass3 of IChildClass3...) and use the same match by tuples with discriminated unions - compiler compiles the project without problems - so it seems like for some reason compiled makes different if - else branches for comparisons by types and comparisons with discriminated unionsInterpellation
@Interpellation As I said - I don't exactly know how the compiler decides what to do. However, compiling discriminated unions is quite different to testing against interfaces. In case of DUs, the compiler knows that only one case will hold. In case of interfaces, a value could match multiple patterns (it can implement multiple interfaces).Tuchman
LOL. I had the same bug in HLVM. flyingfrogblog.blogspot.co.uk/2010/04/…Parabola
@Tomas - thank you for explanation, but rhetoric question why only one condition for each case is not enough (if (original is IChildClass1 && compareWith is IChildClass1) case #1 ...)Interpellation
@Interpellation One condition is enough, as you say. This is just a bug in the code generation part of the pattern match compiler that should factor subexpressions to eliminate such bloat but the author obviously forgot in this instance. Easily done and easily fixed.Parabola

© 2022 - 2024 — McMap. All rights reserved.