Skip to content

Performance: Please consider allowing more control over the layout / size / alignment of fields of records and discriminated unions (case + individual fields) #5215

@zpodlovics

Description

@zpodlovics

Right now F# does not allow any control over the layout (sequential + packing) and size (intentional empty padding to multiply of cache line size to prevent false sharing) and alignment of fields of records and discriminated unions. In same memory or performance critical cases this will prevent using some F# constructs such as records or discriminated unions. Please note, classes are also allowed to have StructLayout attribute and field layout attributes.

This could be important in the compiler and standard library too, as there will a difference between the memory usage and performance of millions of 3 byte structs vs 8 byte objects vs 24 byte objects. It's probably also worth to check the most commonly used / critical data structure types in the F# compiler.

Repro steps

The following program will print the layout of several F# constructs.

open System
open System.Runtime.InteropServices
open System.Runtime.CompilerServices

open ObjectLayoutInspector;


[<Struct; StructLayout(LayoutKind.Sequential, Pack = 1)>]
type V = 
    val Value: byte
    new(v) = { Value = v }

[<Struct>]
type TestStruct =
    val Field1: V
    val Field2: V
    val Field3: V
    new(f1,f2,f3) = { Field1=f1;Field2=f2;Field3=f3}

[<Struct; StructLayout(LayoutKind.Sequential, Pack = 1)>]
type TestStructPacked =
    val Field1: V
    val Field2: V
    val Field3: V
    new(f1,f2,f3) = { Field1=f1;Field2=f2;Field3=f3}

type TestClass =
    val Field1: V
    val Field2: V
    val Field3: V
    new(f1,f2,f3) = { Field1=f1;Field2=f2;Field3=f3}

[<Class; StructLayout(LayoutKind.Sequential, Pack = 1)>]
type TestClassPacked =
    val Field1: V
    val Field2: V
    val Field3: V
    new(f1,f2,f3) = { Field1=f1;Field2=f2;Field3=f3}

type TestRecord = {
    Field1: V
    Field2: V
    Field3: V
}

[<Struct>]
type TestStructRecord = {
    Field1: V
    Field2: V
    Field3: V
}

type TestSingleDU = TestSingleDU of V * V * V

[<Struct>]
type TestStructSingleDU = TestStructSingleDU of V * V * V

type TestDU = 
    | Case1 of V * V * V
    | Case2 of V * V * V

[<Struct>]
type TestStructDU = 
    | Case1 of c1f1:V * c1f2:V * c1f3:V
    | Case2 of c2f1:V * c2f2:V * c2f3:V

[<EntryPoint>]
let main argv =
    TypeLayout.PrintLayout<V> true;
    TypeLayout.PrintLayout<TestStruct> true;
    TypeLayout.PrintLayout<TestStructPacked> true;
    TypeLayout.PrintLayout<TestClass> true;
    TypeLayout.PrintLayout<TestClassPacked> true;
    TypeLayout.PrintLayout<TestRecord> true;
    TypeLayout.PrintLayout<TestStructRecord> true;
    TypeLayout.PrintLayout<TestSingleDU> true;
    TypeLayout.PrintLayout<TestStructSingleDU> true;
    TypeLayout.PrintLayout<TestDU> true;
    TypeLayout.PrintLayout<TestStructDU> true;

    0 // return an integer exit code

Example project:
fsharp_layout.zip

Expected behavior

Have a whitelist for additional attributes to control the size / layout / alignment of fields of records and discriminated unions, at least for sequential + pack=1 case.

Actual behavior

No control over the memory layout of records and discriminated unions.

Type layout for 'V'
Size: 1 bytes. Paddings: 0 bytes (%0 of empty space)
|=============================|
|     0: Byte Value@ (1 byte) |
|=============================|


Type layout for 'TestStruct'
Size: 3 bytes. Paddings: 0 bytes (%0 of empty space)
|===========================|
|     0: V Field1@ (1 byte) |
|---------------------------|
|     1: V Field2@ (1 byte) |
|---------------------------|
|     2: V Field3@ (1 byte) |
|===========================|


Type layout for 'TestStructPacked'
Size: 3 bytes. Paddings: 0 bytes (%0 of empty space)
|===========================|
|     0: V Field1@ (1 byte) |
|---------------------------|
|     1: V Field2@ (1 byte) |
|---------------------------|
|     2: V Field3@ (1 byte) |
|===========================|


Type layout for 'TestClass'
Size: 24 bytes. Paddings: 21 bytes (%87 of empty space)
|============================|
| Object Header (8 bytes)    |
|----------------------------|
| Method Table Ptr (8 bytes) |
|============================|
|     0: V Field1@ (1 byte)  |
|----------------------------|
|   1-7: padding (7 bytes)   |
|----------------------------|
|     8: V Field2@ (1 byte)  |
|----------------------------|
|  9-15: padding (7 bytes)   |
|----------------------------|
|    16: V Field3@ (1 byte)  |
|----------------------------|
| 17-23: padding (7 bytes)   |
|============================|


Type layout for 'TestClassPacked'
Size: 8 bytes. Paddings: 5 bytes (%62 of empty space)
|============================|
| Object Header (8 bytes)    |
|----------------------------|
| Method Table Ptr (8 bytes) |
|============================|
|     0: V Field1@ (1 byte)  |
|----------------------------|
|     1: V Field2@ (1 byte)  |
|----------------------------|
|     2: V Field3@ (1 byte)  |
|----------------------------|
|   3-7: padding (5 bytes)   |
|============================|


Type layout for 'TestRecord'
Size: 24 bytes. Paddings: 21 bytes (%87 of empty space)
|============================|
| Object Header (8 bytes)    |
|----------------------------|
| Method Table Ptr (8 bytes) |
|============================|
|     0: V Field1@ (1 byte)  |
|----------------------------|
|   1-7: padding (7 bytes)   |
|----------------------------|
|     8: V Field2@ (1 byte)  |
|----------------------------|
|  9-15: padding (7 bytes)   |
|----------------------------|
|    16: V Field3@ (1 byte)  |
|----------------------------|
| 17-23: padding (7 bytes)   |
|============================|


Type layout for 'TestStructRecord'
Size: 3 bytes. Paddings: 0 bytes (%0 of empty space)
|===========================|
|     0: V Field1@ (1 byte) |
|---------------------------|
|     1: V Field2@ (1 byte) |
|---------------------------|
|     2: V Field3@ (1 byte) |
|===========================|


Type layout for 'TestSingleDU'
Size: 24 bytes. Paddings: 21 bytes (%87 of empty space)
|============================|
| Object Header (8 bytes)    |
|----------------------------|
| Method Table Ptr (8 bytes) |
|============================|
|     0: V item1 (1 byte)    |
|----------------------------|
|   1-7: padding (7 bytes)   |
|----------------------------|
|     8: V item2 (1 byte)    |
|----------------------------|
|  9-15: padding (7 bytes)   |
|----------------------------|
|    16: V item3 (1 byte)    |
|----------------------------|
| 17-23: padding (7 bytes)   |
|============================|


Type layout for 'TestStructSingleDU'
Size: 3 bytes. Paddings: 0 bytes (%0 of empty space)
|=========================|
|     0: V item1 (1 byte) |
|-------------------------|
|     1: V item2 (1 byte) |
|-------------------------|
|     2: V item3 (1 byte) |
|=========================|


Type layout for 'TestDU'
Size: 8 bytes. Paddings: 0 bytes (%0 of empty space)
|============================|
| Object Header (8 bytes)    |
|----------------------------|
| Method Table Ptr (8 bytes) |
|============================|
|============================|


Type layout for 'TestStructDU'
Size: 12 bytes. Paddings: 2 bytes (%16 of empty space)
|=============================|
|     0: V _c1f1 (1 byte)     |
|-----------------------------|
|     1: V _c1f2 (1 byte)     |
|-----------------------------|
|     2: V _c1f3 (1 byte)     |
|-----------------------------|
|     3: V _c2f1 (1 byte)     |
|-----------------------------|
|     4: V _c2f2 (1 byte)     |
|-----------------------------|
|     5: V _c2f3 (1 byte)     |
|-----------------------------|
|   6-7: padding (2 bytes)    |
|-----------------------------|
|  8-11: Int32 _tag (4 bytes) |
|=============================|

Known workarounds

Do not use F# constructs (records, discriminated unions) where memory usage or performance is critical. As a workaround it would be good to create my own records and discriminated unions manually from fully layout/size/field controlled structs and classes (eg.: by implementing an interface, and mark it as manually created).

Related information

  • Operating system: Ubuntu 16.04 x86_64
  • .NET Runtime:
ii  dotnet-host                                                 2.1.0-1                                                     amd64        Microsoft .NET Core Host - 2.1.0
ii  dotnet-hostfxr-2.0.7                                        2.0.7-1                                                     amd64        Microsoft .NET Core Host FX Resolver - 2.0.7 2.0.7
ii  dotnet-hostfxr-2.1                                          2.1.0-1                                                     amd64        Microsoft .NET Core Host FX Resolver - 2.1.0 2.1.0
ii  dotnet-runtime-2.0.7                                        2.0.7-1                                                     amd64        Microsoft .NET Core Runtime - 2.0.7 Microsoft.NETCore.App 2.0.7
ii  dotnet-runtime-2.1                                          2.1.0-1                                                     amd64        Microsoft .NET Core Runtime - 2.1.0 Microsoft.NETCore.App 2.1.0
ii  dotnet-runtime-deps-2.1                                     2.1.0-1                                                     amd64        dotnet-runtime-deps-2.1 2.1.0
ii  dotnet-sdk-2.1                                              2.1.300-1                                                   amd64        Microsoft .NET Core SDK 2.1.300

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions