$30 off During Our Annual Pro Sale. View Details »

Make the compiler sweat, chill in runtime by Simon Lindholm

Make the compiler sweat, chill in runtime by Simon Lindholm

Malmö C++ User Group - Meeting 0x2 - June 26, 2018

https://www.meetup.com/Malmo-C-User-Group/events/251265413/

Modern C++ offers some powerful features for doing work in compile time instead of in runtime. We'll explore some of these features, for a faster, smaller and more correct runtime.

Simon Lindholm is an independent contractor, mostly working with Linux and various embedded platforms.

Ólafur Waage

June 26, 2018
Tweet

More Decks by Ólafur Waage

Other Decks in Programming

Transcript

  1. Malmö C++ User Group
    Meeting 0x2

    View Slide

  2. Make the compiler sweat,
    chill in runtime
    Malmö C++ User Group
    Meeting 0x2

    View Slide

  3. i.e.
    Malmö C++ User Group
    Meeting 0x2

    View Slide

  4. Doing stuff in compile time
    Malmö C++ User Group
    Meeting 0x2

    View Slide

  5. i.e.
    Malmö C++ User Group
    Meeting 0x2

    View Slide

  6. ...metaprogramming
    Malmö C++ User Group
    Meeting 0x2

    View Slide

  7. 7
    Who am I?

    Started programming BASIC on C64 and
    Apple II

    Borland C++ 3.0 ca. 1994

    Studied at LTH

    Worked mostly with embedded stuf

    View Slide

  8. 8
    Embedded systems

    Limited memory

    Limited CPU power

    Important with correctness

    Mostly dominated by C

    Somewhat conservative attitude towards
    C++ (C with classes...)

    View Slide

  9. 9
    The process leading up to compile-time trickery

    “It would be really nice if it was possible
    to...”

    “Is it possible?”

    “It is!”

    ... or “...well, sort of”

    View Slide

  10. 10
    What’s this talk about?

    Just go through a couple of these ideas

    Nothing new under the sun

    These ideas presented as inspiration

    View Slide

  11. 11
    Like a fashion show

    View Slide

  12. Let’s just dive in...

    View Slide

  13. Example 1:
    CRC32

    View Slide

  14. 14
    CRC32

    CRC = Cyclic Redundancy Check

    Many diferent variations

    32 bit most common size

    Robust against burst errors

    View Slide

  15. 15
    CRC32: Basic idea
    Data CRC32
    Data
    CRC32
    CRC32
    CRC32()
    CRC32()
    Same?
    Alice
    Bob

    View Slide

  16. 16
    CRC32: Polynomials

    The algorithm is specified by a
    polynomial

    A trillion diferent standards

    Ethernet, ZIP, etc.:
    x32 + x26 + x23 + x22 + x16 + x12 + x11 + x10 + x8 + x7 + x5 + x4 + x2 + x + 1

    ...or just 0xEDB88320

    Can just treat it as a magic number

    View Slide

  17. 17
    CRC32: API

    Basically just want a class:
    – Constructor: CRC32(uint32_t polynomial)
    – Method: add(const uint8_t * data, size_t N)
    – Method: uint32_t result()

    View Slide

  18. CRC32
    Implementation 1
    Simple & slow

    View Slide

  19. 19
    CRC32 - Implementation 1

    View Slide

  20. View Slide

  21. 21
    CRC32 - Implementation 1

    Calls calc_crc_value() for each byte

    Pure function - output only depends
    on the input

    Only 256 possible inputs

    → Precomputation table!

    View Slide

  22. CRC32
    Implementation 2
    Precomputation table

    View Slide

  23. 23
    CRC32 - Implementation 2

    View Slide

  24. View Slide

  25. 25
    CRC32 - Implementation 2

    Table lookup instead of
    calc_crc_value() for each byte

    Takes about 1K memory (256 * 4)

    The code to generate the table also
    takes a small amount of memory

    Less flexible - can’t choose polynomial
    at runtime (could get around this...)

    View Slide

  26. CRC32
    Implementation 3
    Static precomputation table

    View Slide

  27. 27
    CRC32 - Implementation 3
    (Identical to version 2)

    View Slide

  28. View Slide

  29. 29
    CRC32 - Implementation 3

    Don’t have to generate the table in
    runtime

    Gets rid of the code to generate the
    table

    Very inflexible

    View Slide

  30. CRC32
    Implementation 4

    Precomputation table

    Static

    Flexible

    View Slide

  31. 31
    CRC32 - Implementation 4

    View Slide

  32. 32
    make_compile_time_array() ?
    (seq & gen_seq stolen from
    Stack Overflow)

    View Slide

  33. 33
    make_compile_time_array() ?

    View Slide

  34. 34
    make_compile_time_array() ?

    View Slide

  35. 35
    make_compile_time_array() ?

    View Slide

  36. 36
    make_compile_time_array() ?

    View Slide

  37. 37
    CRC32 - Implementation 4

    View Slide

  38. 38
    CRC32 - Implementation 4

    Don’t have to generate the table in
    runtime

    Gets rid of the code to generate the
    table

    Pretty flexible

    General strategy for any lookup table
    (sin, cos, etc.?)

    View Slide

  39. Example 2:
    Safe(r) buffers

    View Slide

  40. 40
    C-style bufer handling

    Pointer arithmetic: uint8_t *

    memset(), memcpy(), memcmp()

    Common when doing low-level stuf
    (raw access to pixel bufer, etc.)

    Error prone

    A lot of bounds checking can be had in
    compile time instead of runtime

    View Slide

  41. 41
    Benefits of compile-time bounds checks

    Catch errors earlier (compiler error)

    Can skip those checks in runtime

    Safer code

    Faster code

    Smaller code

    View Slide

  42. 42
    We make some bufer classes
    FixedConstBuffer ConstBuffer
    Buffer
    FixedBuffer

    View Slide

  43. Buffer classes
    FixedConstBuffer ConstBuffer
    Buffer
    FixedBuffer
    Const
    Non-Const
    Size known at compile-
    time
    Size only known in runtime

    View Slide

  44. Buffer classes
    FixedConstBuffer ConstBuffer
    Buffer
    FixedBuffer
    DynamicBuffer
    StaticBuffer
    std::unique_ptr
    uint8_t buf_[N]
    Const
    Non-Const
    Size known at compile-
    time
    Size only known in runtime

    View Slide

  45. 45
    Class skeletons

    View Slide

  46. 46
    Class skeletons

    View Slide

  47. 47
    Class skeletons

    View Slide

  48. 48
    Class skeletons
    (No ownership of data)

    View Slide

  49. 49
    Class skeletons

    View Slide

  50. 50
    Class skeletons
    (Owners of data)

    View Slide

  51. 51
    The ownership classes

    StaticBufer
    – As the name suggest, pretty static
    – Put it on the stack, or as static data

    DynamicBufer
    – Essentially just wrapper of std::unique_ptr
    – So, std::move() if you want to transfer ownership

    View Slide

  52. 52
    The wrapper classes
    Bufer, ConstBufer, FixedBufer<>, FixedConstBufer<>

    FixedBufer<> & FixedConstBufer<>
    – Single pointer (uint8_t* or const uint8_t*)

    Bufer & ConstBufer
    – Pointer + std::size

    Hence, they are meant for pass-by-value
    Do this! Not this!

    View Slide

  53. 53
    Methods

    take(I,N) & skip(I) Pointer arithmetic

    set_zero(), fill_with(X) memset()

    copy(dst, src) memcpy()

    equals(buf1, buf2) memcmp()

    operator<<(std::ostream&) [debugging..]

    ... tons of overloads for these...

    View Slide

  54. 54
    Example: StaticBufer<>

    View Slide

  55. 55
    Example: StaticBufer<>

    View Slide

  56. 56
    Example: StaticBufer<>
    Compile error!

    View Slide

  57. 57
    static_assert()

    View Slide

  58. 58
    Example: DynamicBufer

    View Slide

  59. 59
    Example: DynamicBufer
    Runtime error!

    View Slide

  60. 60
    Copying & comparing
    Is essentially equivalent to:
    ...but with proper bounds checking

    View Slide

  61. 61
    Copying & comparing

    If both bufer types fixed, bounds checking
    happen at compile time

    Otherwise, in runtime

    View Slide

  62. 62
    Assembly instructions
    ...just a few instructions (with optimizations enabled)

    View Slide

  63. 63
    Summary & reflections

    These bufer classes by no means perfect

    Should implement operator overloading so
    they behave the same as C-style pointers

    Complete brain-fart to implement copy(),
    equals() etc. as standalone functions

    Main point:
    Most of the bounds checks that can be done
    in compile time are being done in compile
    time.

    View Slide

  64. Bonus round:
    Parsing JSON

    View Slide

  65. 65
    Why in the world would you want to?

    Data you want to integrate into your
    code somehow
    – Cross-language message specifications
    – Hardware configurations
    – etc.

    JSON: Common, convenient, simple

    View Slide

  66. 66
    Generate C++ code with script?

    Depending on build system, can be messy to
    integrate

    Adds requirement of Python etc for the build
    system

    Actually pretty convenient most of the time

    *BUT* aesthetically displeasing to have to
    resort to a separate language to do something
    that SHOULD be possible in pure C++

    View Slide

  67. 67
    What’s really our goal here?

    Parsing JSON is actually pretty simple. It’s
    really just:
    – Numbers (12, -34, 0.23, 1.3e-4, etc.)
    – Strings (“foo”, “bar”, “hey\nho!”)
    – Booleans (true / false)
    – Lists [ 1, 2, “foo”, [ ] ]
    – Dictionaries { “foo”: 129, “bar”: [1,2,3] }
    – null

    ...but we want to do it in compile time now

    View Slide

  68. 68
    What’s really our goal here?

    Let’s not spend too much time on preamble

    ...just dive in head first and see where we end
    up

    View Slide

  69. 69
    Some limitations...

    Template parameters are most often types
    (std::vector)

    ...but perfectly fine to use values as well
    (std::array)

    But not all values are welcome, we aren’t
    allowed to use strings as template arguments,
    for instance

    View Slide

  70. 70
    Some limitations...
    This is fine

    View Slide

  71. 71
    Some limitations...
    This is fine
    This is not

    View Slide

  72. 72
    Let’s combine a few things

    Variadic templates

    User-defined literals

    ...and unfortunately a C-macro (yuck!)

    View Slide

  73. 73
    ...and suddenly we can use strings as template
    arguments

    View Slide

  74. 74
    We can print the strings easily (useful for
    debugging)
    ...but let’s do something a bit more general,
    (becuase we will end up wanting to print quite a lot of
    diferent types)

    View Slide

  75. But let’s make something a bit more general

    View Slide

  76. 76
    And now we can print our strings thusly:
    Before:
    After:
    Almost the same, but the $<> notation is
    trivial to extend to other types - and we’ll
    have a lot of them

    View Slide

  77. 77
    How do we read the JSON data to begin with?

    Ideally, we would just want to be able to read it
    as a regular file

    Not possible in compile time - there are no
    constexpr I/O functions

    We can almost use #include, but not quite

    With can get something reasonably
    satisfactory using raw string literals

    View Slide

  78. 78
    Alt 1. Embed the JSON directly into the C++ code
    Look somewhat Ok, but we have to copy/paste
    our JSON data into our code - not nice...

    View Slide

  79. 79
    Alt 2. #include the JSON data
    JSON somewhat more independent from the C+
    + code, but we need it wrapped in R”( )”_js

    View Slide

  80. 80
    meta::list

    Skipping a ton of details here

    But let’s look at a general list class/template
    we’ll be using quite a bit

    Really just a wrapper for std::tuple, but with a
    bunch of helpers

    View Slide

  81. meta::list

    View Slide

  82. 82
    Some examples of list methods

    View Slide

  83. 83
    Of course have a bunch of similar string methods

    View Slide

  84. 84
    Some are really trivial

    View Slide

  85. 85
    Some are pretty simple

    View Slide

  86. 86
    Some are a bit more complex
    Remember
    seq/gen_seq from
    CRC32?

    View Slide

  87. 87
    With the two meta-classes list and str, we can start building a
    parser

    The stuff we want to parse out is
    – Numbers (12, -34, 0.23, 1.3e-4, etc.)
    – Strings (“foo”, “bar”, “hey\nho!”)
    – Booleans (true / false)
    – Lists [ 1, 2, “foo”, [ ] ]
    – Dictionaries { “foo”: 129, “bar”:
    [1,2,3] }
    – null

    (For simplicity, we won’t support floats -
    only integers)

    View Slide

  88. 88
    Our building blocks for the data, will be

    View Slide

  89. 89
    Our building blocks for the data, will be

    View Slide

  90. 90
    Our building blocks for the data, will be

    View Slide

  91. 91
    Our building blocks for the data, will be

    View Slide

  92. 92
    Our building blocks for the data, will be
    (Dictionaries will be treated as just lists of
    key-value pairs)

    View Slide

  93. 93
    Mapping JSON <--> types

    So if we have some JSON data like this:

    Then we want our resulting type to be
    something like this:

    View Slide

  94. 94
    The most basic parser: Literal

    View Slide

  95. (I think I’ve overdone the Comic Sans joke...)

    View Slide

  96. 96
    The most basic parser: Literal

    View Slide

  97. 97
    The most basic parser: Literal
    Helper to create
    successful/failed result-types

    View Slide

  98. 98
    Next most basic parser: from_func
    (could do with a better name)

    View Slide

  99. 99
    Next most basic parser: from_func
    (could do with a better name)
    So, if we only have some constexpr versions of the
    functions...

    View Slide

  100. 100
    Next most basic parser: from_func
    (could do with a better name)
    Then we can use our from_func as such:
    And we have equivalents of the simple regexes:

    View Slide

  101. 101
    A few other basic parsers
    ...which lets us combine other parsers

    View Slide

  102. 102
    A few other basic parsers
    ...which lets us combine other parsers

    View Slide

  103. 103
    A few other basic parsers
    ...which lets us combine other parsers

    View Slide

  104. 104
    A few other basic parsers
    ...which lets us combine other parsers

    View Slide

  105. 105
    Putting it all together

    View Slide

  106. 106
    Putting it all together

    View Slide

  107. 107
    A small test program

    View Slide

  108. Compiling

    View Slide

  109. 109
    JSON: In summary

    It’s sort of doable to parse JSON at
    compile time

    This implementation is ridiculously
    slow

    Don’t do stuf like this in real projects

    ...but it does show that you CAN do a lot of
    things in compile time that might not have
    been previously possible

    View Slide

  110. That’s all I had

    View Slide

  111. You can find the code on:
    datakod.se/gitlab

    View Slide

  112. Just a fashion show

    View Slide

  113. Thank you for listening!

    View Slide

  114. Thank you for listening!
    Malmö C++ User Group
    Meeting 0x2
    datakod.se/gitlab

    View Slide