Skip to content

Take purity modeling seriously#43852

Merged
Keno merged 20 commits intomasterfrom
kf/effectsstaging
Feb 9, 2022
Merged

Take purity modeling seriously#43852
Keno merged 20 commits intomasterfrom
kf/effectsstaging

Conversation

@Keno
Copy link
Copy Markdown
Member

@Keno Keno commented Jan 18, 2022

TLDR

Before:

julia> let b = Expr(:block, (:(y += sin($x)) for x in randn(1000))...)
           @eval function f_sin_perf()
               y = 0.0
               $b
               y
           end
       end
f_sin_perf (generic function with 1 method)

julia> @time @code_typed f_sin_perf()
 15.707267 seconds (25.95 M allocations: 1.491 GiB, 3.30% gc time)
[lots of junk]

After:

julia> @time @code_typed f_sin_perf()
  0.016818 seconds (187.35 k allocations: 7.901 MiB, 99.73% compilation time)
CodeInfo(
1 ─     return 27.639138714768546
) => Float64

so roughly a 1000x improvement in compile time performance for const-prop heavy functions.

There are also run time improvements for functions that have patterns like:

function some_function_to_big_to_be_inlined_but_pure(x)
....
end

function foo(x)
some_function_to_big_to_be_inlined_but_pure(x)
return x
end

The inliner will now be able to see that some_function_to_big_to_be_inlined_but_pure is effect free, even without inlining it and just delete it, improving runtime performance (if some_function_to_big_to_be_inlined_but_pure is small enough to be inlined, there is a small compile time throughput win, by being able to delete it without inlining, but that's a smaller gain than the compile time gain above).

Motivation / Overview

There are two motivations for this work. The first is the above mentioned improvement in compiler performance for const-prop heavy functions. This comes up a fair bit in various Modeling & Simulation codes we have where Julia code is often auto-generated from some combination of parameterized model codes and data. This ends up creating enormous functions with significant need for constant propagation (~50k statements with ~20k constant calls are not uncommon). Our current compiler was designed for people occasionally throwing a sqrt(2) or something in a function, not 20k of them, so performance is quite bad.

The second motivation is to have finer grained control over our purity modeling. We have @Base.pure, but that has somewhat nebulous semantics and is quite a big hammer that is not appropriate in most situations.

These may seem like orthogonal concerns at first, but they are not. The compile time issues fundamentally stem from us running constant propagation in inference's abstract interpreter. However, for simple, pure functions, that is entirely unnecessary, because we have a super-fast, JIT compiler version of that function just laying around in general. The issue is that we currently, we generally do not know when it is legal to run the JIT-compiled version of the function and when we need to abstractly interpret it. However, if the compiler were able to figure out an appropriate notion of purity, it could start doing that (which is what it does now for @Base.pure functions).

This PR adds that kind of notion of purity, converges it along with type information during inference and then makes use of it to speed up evaluation of constant propagation (where it is legal to do so), as well as improving the inliner.

The new purity notions

The new purity model consists of four different kinds flags per code instance. For builtins and intrinsics the existing effect free and nothrow models are re-used. There is also a new macro @Base.assume_effects available, which can set the purity base case for methods or :foreigncalls. Here is the docstring for that macro, which also explains the semantics of the new purity flags:

    @assume_effects setting... ex
    @assume_effects(setting..., ex)

`@assume_effects` overrides the compiler's effect modeling for the given method.
`ex` must be a method definition.

WARNING: Improper use of this macro causes undefined behavior (including crashes,
incorrect answers, or other hard to track bugs). Use with care an only if absolutely
required.

In general, each `setting` value makes an assertion about the behavior of the
function, without requiring the compiler to prove that this behavior is indeed
true. These assertions are made for all world ages. It is thus advisable to limit
the use of generic functions that may later be extended to invalidate the
assumption (which would cause undefined behavior).

The following `settings` are supported.
# `:consistent`

The `:consistent` setting asserts that for egal inputs:
    - The manner of termination (return value, exception, non-termination) will always be the same.
    - If the method returns, the results will always be egal.

Note: This in particular implies that the return value of the method must be
      immutable. Multiple allocations of mutable objects (even with identical
      contents) are not egal.

Note: The idempotency assertion is made world-arge wise. More formally, write
      fₐ for the evaluation of `f` in world-age `a`, then we require:

          ∀ a, x, y: x === y → fₐ(x) === fₐ(y)

      However, for two world ages `a, b` s.t. `a != b`, we may have `fₐ(x) !== fₐ(y)``

Note: A further implication is that idempontent functions may not make their
      return value dependent on the state of the heap or any other global state
      that is not constant for a given world age.

Note: The idempontency includes all legal rewrites performed by the optimizizer.
      For example, floating-point fastmath operations are not considered idempotent,
      because the optimizer may rewrite them causing the output to not be idempotent,
      even for the same world age (e.g. because one ran in the interpreter, while
      the other was optimized).

# `:effect_free`

The `:effect_free` setting asserts that the method is free of externally semantically
visible side effects. The following is an incomplete list of externally semantically
visible side effects:

 - Changing the value of a global variable.
 - Mutating the heap (e.g. an array or mutable value), except as noted below
 - Changing the method table (e.g. through calls to eval)
 - File/Network/etc. I/O
 - Task switching

However, the following are explicitly not semantically visible, even if they
may be observable:

 - Memory allocations (both mutable and immutable)
 - Elapsed time
 - Garbage collection
 - Heap mutations of objects whose lifetime does not exceed the method (i.e.
   were allocated in the method and do not escape).
 - The returned value (which is externally visible, but not a side effect)

The rule of thumb here is that an externally visible side effect is anything
that would affect the execution of the remainder of the program if the function
were not executed.

Note: The effect free assertion is made both for the method itself and any code
      that is executed by the method. Keep in mind that the assertion must be
      valid for all world ages and limit use of this assertion accordingly.

# `:nothrow`

The `:nothrow` settings asserts that this method does not terminate abnormally
(i.e. will either always return a value or never return).

Note: It is permissible for :nothrow annotated methods to make use of exception
      handling internally as long as the exception is not rethrown out of the
      method itself.

Note: MethodErrors and similar exceptions count as abnormal termination.

# `:terminates_globally`

The `:terminates_globally` settings asserts that this method will eventually terminate
(either normally or abnormally), i.e. does not infinite loop.

Note: The compiler will consider this a strong indication that the method will
      terminate relatively *quickly* and may (if otherwise legal), call this
      method at compile time. I.e. it is a bad idea to annotate this setting
      on a method that *technically*, but not *practically*, terminates.

Note: The `terminates_globally` assertion, covers any other methods called by
      the annotated method.

# `:terminates_locally`

The `:terminates_locally` setting is like `:terminates_globally`, except that it only
applies to syntactic control flow *within* the annotated method. It is this
a much weaker (and thus safer) assertion that allows for the possibility of
non-termination if the method calls some other method that does not terminate.

Note: `terminates_globally` implies `terminates_locally`.

# `:total`

The `setting` combines the following other assertions:
    - `:consistent`
    - `:effect_free`
    - `:nothrow`
    - `:terminates_globally`
and is a convenient shortcut.

Note: `@assume_effects :total` is similar to `@Base.pure` with the primary
      distinction that the idempotency requirement applies world-age wise rather
      than globally as described above. However, in particular, a method annotated
      `@Base.pure` is always total.

Changes to data structures

  • Each CodeInstance gains two sets of four flags corresponding to the notions above (except terminates_locally, which is just a type inference flag). One set of flags tracks IPO-valid information (as determined by inference), the other set of flags tracks optimizer-valid information (as determined after optimization). Otherwise they have identical semantics.

  • Method and CodeInfo each gain 5 bit flags corresponding 1:1 to the purity notions defined above. No separate distinction is made between IPO valid and optimizer valid flags here. We might in the future want such a distinction, but I'm hoping to get away without it for now, since the IPO-vs-optimizer distinction is a bit subtle and I don't really want to expose that to the user.

  • :foreigncall gains an extra argument (after cconv) to describe the effects of the call.

Algorithm

Relatively straightforward.

  • Every call or builtin accumulates its effect information into the current frame.
  • Finding an effect (throw/global side effect/non-idempotenct, etc.) taints the entire frame. Idempotency is technically a dataflow property, but that is not modeled here and any non-idempotent intrinsic will taint the idempotency flag, even if it does not contribute to the return value. I don't think that's a huge problem in practice, because currently we only use idempotency if effect-free is also set and in effect-free functions you'd generally expect every statement to contribute to the return value.
  • Any backedge taints the termination effect, as does any recursion
  • Unknown statements (generic calls, things I haven't gotten around to) taint all effects

TODO

  • Constant Evaluation needs to be moved into the correct world age
  • Cleanup
  • PkgEval

@simeonschaub
Copy link
Copy Markdown
Member

simeonschaub commented Jan 18, 2022

Two quick questions:

Note: The idempontency includes all legal rewrites performed by the optimizizer. For example, floating-point fastmath operations are not considered idempotent, because the optimizer may rewrite them causing the output to not be idempotent, even for the same world age (e.g. because one ran in the interpreter, while the other was optimized).

Does this include code calling back into the optimizer, e.g. via return_type? Might be good to make that explicit in the docs.

:effect_free

The :effect_free setting asserts that the method is free of externally semantically
visible side effects. The following is an incomplete list of externally semantically
visible side effects:

Does :effect_free always imply :nothrow? Since that's technically externally visible IIUC.

@tkf
Copy link
Copy Markdown
Member

tkf commented Jan 18, 2022

The :idempotent setting asserts that for egal inputs:

  • The manner of termination (return value, exception, non-termination) will always be the same.
  • If the method returns, the results will always be egal.

Note: This in particular implies that the return value of the method must be immutable.

How does one show the implication? A function f defined as

const C = []
f() = C

seems to satisfy the assertions but C is not immutable.

Also, since :idempotent does not mention mutation, I wonder if

const COUNTER = Ref(0)

function g!(arg::RefValue{Int})
    arg[] = COUNTER[] += 1
    return 0
end

is considered :idempotent? Calling g! idempotent is counter-intuitive to me but is it a standard notation for this type of property? Or do I misread the conditions?

@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 18, 2022

Two quick questions:

Note: The idempontency includes all legal rewrites performed by the optimizizer. For example, floating-point fastmath operations are not considered idempotent, because the optimizer may rewrite them causing the output to not be idempotent, even for the same world age (e.g. because one ran in the interpreter, while the other was optimized).

Does this include code calling back into the optimizer, e.g. via return_type? Might be good to make that explicit in the docs.

return_type is considered idempotent whenever its tfunc returns Const. Const also implies IPO safety. It is possible that return_type sometimes violates this, but that's already a soundness bug, c.f.

julia/base/compiler/tfuncs.jl

Lines 1836 to 1838 in 294b0df

# TODO: this function is a very buggy and poor model of the return_type function
# since abstract_call_gf_by_type is a very inaccurate model of _method and of typeinf_type,
# while this assumes that it is an absolutely precise and accurate and exact model of both

:effect_free

The :effect_free setting asserts that the method is free of externally semantically
visible side effects. The following is an incomplete list of externally semantically
visible side effects:

Does :effect_free always imply :nothrow? Since that's technically externally visible IIUC.

No, the thrown value is treated like the return value, I can write that down explicitly.

@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 18, 2022

The :idempotent setting asserts that for egal inputs:

  • The manner of termination (return value, exception, non-termination) will always be the same.
  • If the method returns, the results will always be egal.

Note: This in particular implies that the return value of the method must be immutable.

How does one show the implication? A function f defined as

const C = []
f() = C

seems to satisfy the assertions but C is not immutable.

You are correct, this is :idempotent - what I meant was that if you allocate a new mutable object, even if the same content, it will not be considered idempotent, because the identity is different. I'll clean up the comment.

Also, since :idempotent does not mention mutation, I wonder if

const COUNTER = Ref(0)

function g!(arg::RefValue{Int})
    arg[] = COUNTER[] += 1
    return 0
end

is considered :idempotent? Calling g! idempotent is counter-intuitive to me but is it a standard notation for this type of property? Or do I misread the conditions?

Yes, it's considered :idempotent, but not :effect_free. Idempotency ignores side effects (in our definition).

@ianatol ianatol self-requested a review January 18, 2022 16:45
@vchuravy vchuravy requested a review from aviatesk January 18, 2022 17:32
@tkf
Copy link
Copy Markdown
Member

tkf commented Jan 18, 2022

In what sense :idempotent relates to the standard usages of idempotent? I can't find any similar usages in https://en.wikipedia.org/wiki/Idempotence.

@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 18, 2022

You're correct. I named these before I had nailed down the exact semantics I wanted. I'll think about a better name, I'm thinking stable might work or maybe something else.

@oscardssmith
Copy link
Copy Markdown
Member

stable seems bad some 3 it has nothing to do with your stability

@tkf
Copy link
Copy Markdown
Member

tkf commented Jan 19, 2022

I think I get why :stable makes sense but I find it a bit less descriptive than the names used for other notions of purity. I wonder if more descriptive names like :returns_same/:returns_egal/:transparent_result/... can work.

@ianatol
Copy link
Copy Markdown
Member

ianatol commented Jan 19, 2022

Re :idempotency naming, the definition in base/expr.jl seems a bit like observational equivalence to me. So maybe :obs_equiv?

@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 19, 2022

Re :idempotency naming, the definition in base/expr.jl seems a bit like observational equivalence to me. So maybe :obs_equiv?

Observational equivalence is the definition of the egality property, but here you're not really asserting observational equivalence. There may be observable side effects. Maybe just :preserves_egal? Though there are more conditions than just that - e.g. if it throws, the property requires it to always throw. Other options are :consistent, or :independent (it behaves the same independent of the environment it executes in).

@tkf
Copy link
Copy Markdown
Member

tkf commented Jan 19, 2022

:preserves_egal, :consistent, and :independent all LGTM. I find :independent puzzling at first but then it feels most adequate among them.

BTW, does it also require the thrown exceptions to be egal? I guess not (as not specified)?

@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 20, 2022

BTW, does it also require the thrown exceptions to be egal? I guess not (as not specified)?

It does not. I considered requiring it, but it's usually false. E.g. MethodErrors get freshly allocated every time.

Copy link
Copy Markdown
Contributor

@jonas-schulze jonas-schulze left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some typos in the docstring

@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 22, 2022

:preserves_egal, :consistent, and :independent all LGTM. I find :independent puzzling at first but then it feels most adequate among them.

I'm gonna go with :state_independent for now, because I think it captures what I mean best: The return/exit behavior is independent of the state of the system.

@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 22, 2022

I'm gonna go with :state_independent for now, because I think it captures what I mean best: The return/exit behavior is independent of the state of the system.

Although actually, now that I'm doing the replacement, I don't like it anymore, since it also says something about optimizer behavior, which doesn't really have anything to do with the state.

@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 22, 2022

Let's go with :consistent then. If I look that up in the dictionary, it says

"unchanging in nature, standard, or effect over time."

which seems like it fits well.

@Keno Keno force-pushed the kf/effectsstaging branch from 19a2bb4 to 3449a16 Compare January 22, 2022 03:04
@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 22, 2022

Rebased our everything that got merged independently and squashed the history.

@Keno Keno force-pushed the kf/effectsstaging branch 2 times, most recently from b276bad to 2474e03 Compare January 22, 2022 06:59
@Keno Keno changed the title WIP: Take purity modeling seriously Take purity modeling seriously Jan 22, 2022
@Keno Keno force-pushed the kf/effectsstaging branch from 2474e03 to 7aaef28 Compare January 22, 2022 07:03
@Keno
Copy link
Copy Markdown
Member Author

Keno commented Jan 22, 2022

I think this is in a reasonable state now. There's more that could be done with this, but I think this is a pretty good place to pause.

Keno added a commit to JuliaDebug/JuliaInterpreter.jl that referenced this pull request Jan 22, 2022
This is for JuliaLang/julia#43852.

Probably hold off on merging until the review phase is through,
but the PR is here for people who want to work on the branch
and need Revise to work.
@timholy timholy added the latency Latency label Jan 22, 2022
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this pull request Feb 17, 2022
In JuliaLang#43852 we noticed that the compiler is getting good enough to
completely DCE a number of our benchmarks. We need to add some sort
of mechanism to prevent the compiler from doing so. This adds just
such an intrinsic. The intrinsic itself doesn't do anything, but
it is considered effectful by our optimizer, preventing it from
being DCE'd. At the LLVM level, it turns into call to an external
varargs function.

The docs for the new intrinsic are as follows:
```
    donotdelete(args...)

This function prevents dead-code elimination (DCE) of itself and any arguments
passed to it, but is otherwise the lightest barrier possible. In particular,
it is not a GC safepoint, does model an observable heap effect, does not expand
to any code itself and may be re-ordered with respect to other side effects
(though the total number of executions may not change).

A useful model for this function is that it hashes all memory `reachable` from
args and escapes this information through some observable side-channel that does
not otherwise impact program behavior. Of course that's just a model. The
function does nothing and returns `nothing`.

This is intended for use in benchmarks that want to guarantee that `args` are
actually computed. (Otherwise DCE may see that the result of the benchmark is
unused and delete the entire benchmark code).

**Note**: `donotdelete` does not affect constant foloding. For example, in
          `donotdelete(1+1)`, no add instruction needs to be executed at runtime and
          the code is semantically equivalent to `donotdelete(2).`

*# Examples

function loop()
    for i = 1:1000
        # The complier must guarantee that there are 1000 program points (in the correct
       	# order) at which the value of `i` is in a register, but has otherwise
        # total control over the program.
        donotdelete(i)
    end
end
```
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this pull request Feb 17, 2022
When union splitting, we currently emit a fallback block that prints
`fatal error in type inference (type bound)`. This has always been an oddity,
because there are plenty of other places in the compiler that we
rely on the correctness of inference, but it has caught a number of
issues over the years and we do still have a few issues (e.g. JuliaLang#43064)
that show this error. Nevertheless, the occurrence of issues like
this has become less frequent, so it might be time to turn it off soon.
At the same time, we have downstream users of the compiler infrastructure
that get confused by having extra `throw` calls inserted into functions
that (post-JuliaLang#43852) were inferred as `:nothrow`.

Here we add an optimization param (defaulted to on) to determine whether
or not to insert the unionsplit fallback block. Because of the conservative
default, we can decide later what the correct default is (maybe turn it on
in `debug` mode?), while letting downstream consumers play with the setting
for now to see if any issues crop up.
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this pull request Feb 17, 2022
* Implement new effect system

* TLDR

Before:
```
julia> let b = Expr(:block, (:(y += sin($x)) for x in randn(1000))...)
           @eval function f_sin_perf()
               y = 0.0
               $b
               y
           end
       end
f_sin_perf (generic function with 1 method)

julia> @time @code_typed f_sin_perf()
 15.707267 seconds (25.95 M allocations: 1.491 GiB, 3.30% gc time)
[lots of junk]
```

After:
```
julia> @time @code_typed f_sin_perf()
  0.016818 seconds (187.35 k allocations: 7.901 MiB, 99.73% compilation time)
CodeInfo(
1 ─     return 27.639138714768546
) => Float64
```

so roughly a 1000x improvement in compile time performance for const-prop heavy functions.

There are also run time improvements for functions that have patterns like:
```
function some_function_to_big_to_be_inlined_but_pure(x)
....
end

function foo(x)
some_function_to_big_to_be_inlined_but_pure(x)
return x
end
```

The inliner will now be able to see that some_function_to_big_to_be_inlined_but_pure is effect free, even without inlining it and just delete it, improving runtime performance (if some_function_to_big_to_be_inlined_but_pure is small enough to be inlined, there is a small compile time throughput win, by being able to delete it without inlining, but that's a smaller gain than the compile time gain above).

* Motivation / Overview

There are two motivations for this work. The first is the above mentioned improvement in compiler performance for const-prop heavy functions. This comes up a fair bit in various Modeling & Simulation codes we have where Julia code is often auto-generated from some combination of parameterized model codes and data. This ends up creating enormous functions with significant need for constant propagation (~50k statements with ~20k constant calls are not uncommon). Our current compiler was designed for people occasionally throwing a `sqrt(2)` or something in a function, not 20k of them, so performance is quite bad.

The second motivation is to have finer grained control over our purity modeling. We have `@Base.pure`, but that has somewhat nebulous semantics and is quite a big hammer that is not appropriate in most situations.

These may seem like orthogonal concerns at first, but they are not. The compile time issues fundamentally stem from us running constant propagation in inference's abstract interpreter. However, for simple, pure functions, that is entirely unnecessary, because we have a super-fast, JIT compiler version of that function just laying around in general. The issue is that we currently, we generally do not know when it is legal to run the JIT-compiled version of the function and when we need to abstractly interpret it. However, if the compiler were able to figure out an appropriate notion of purity, it could start doing that (which is what it does now for `@Base.pure` functions).

This PR adds that kind of notion of purity, converges it along with type information during inference and then makes use of it to speed up evaluation of constant propagation (where it is legal to do so), as well as improving the inliner.

* The new purity notions

The new purity model consists of four different kinds flags per code instance. For builtins and intrinsics the existing effect free and nothrow models are re-used. There is also a new macro `@Base.assume_effects` available, which can set the purity base case for methods or `:foreigncall`s. Here is the docstring for that macro, which also explains the semantics of the new purity flags:

```
    @assume_effects setting... ex
    @assume_effects(setting..., ex)

`@assume_effects` overrides the compiler's effect modeling for the given method.
`ex` must be a method definition.

WARNING: Improper use of this macro causes undefined behavior (including crashes,
incorrect answers, or other hard to track bugs). Use with care an only if absolutely
required.

In general, each `setting` value makes an assertion about the behavior of the
function, without requiring the compiler to prove that this behavior is indeed
true. These assertions are made for all world ages. It is thus advisable to limit
the use of generic functions that may later be extended to invalidate the
assumption (which would cause undefined behavior).

The following `settings` are supported.
** `:idempotent`

The `:idempotent` setting asserts that for egal inputs:
    - The manner of termination (return value, exception, non-termination) will always be the same.
    - If the method returns, the results will always be egal.

Note: This in particular implies that the return value of the method must be
      immutable. Multiple allocations of mutable objects (even with identical
      contents) are not egal.

Note: The idempotency assertion is made world-arge wise. More formally, write
      fₐ for the evaluation of `f` in world-age `a`, then we require:

          ∀ a, x, y: x === y → fₐ(x) === fₐ(y)

      However, for two world ages `a, b` s.t. `a != b`, we may have `fₐ(x) !== fₐ(y)``

Note: A further implication is that idempontent functions may not make their
      return value dependent on the state of the heap or any other global state
      that is not constant for a given world age.

Note: The idempontency includes all legal rewrites performed by the optimizizer.
      For example, floating-point fastmath operations are not considered idempotent,
      because the optimizer may rewrite them causing the output to not be idempotent,
      even for the same world age (e.g. because one ran in the interpreter, while
      the other was optimized).

** `:effect_free`

The `:effect_free` setting asserts that the method is free of externally semantically
visible side effects. The following is an incomplete list of externally semantically
visible side effects:

 - Changing the value of a global variable.
 - Mutating the heap (e.g. an array or mutable value), except as noted below
 - Changing the method table (e.g. through calls to eval)
 - File/Network/etc. I/O
 - Task switching

However, the following are explicitly not semantically visible, even if they
may be observable:

 - Memory allocations (both mutable and immutable)
 - Elapsed time
 - Garbage collection
 - Heap mutations of objects whose lifetime does not exceed the method (i.e.
   were allocated in the method and do not escape).
 - The returned value (which is externally visible, but not a side effect)

The rule of thumb here is that an externally visible side effect is anything
that would affect the execution of the remainder of the program if the function
were not executed.

Note: The effect free assertion is made both for the method itself and any code
      that is executed by the method. Keep in mind that the assertion must be
      valid for all world ages and limit use of this assertion accordingly.

** `:nothrow`

The `:nothrow` settings asserts that this method does not terminate abnormally
(i.e. will either always return a value or never return).

Note: It is permissible for :nothrow annotated methods to make use of exception
      handling internally as long as the exception is not rethrown out of the
      method itself.

Note: MethodErrors and similar exceptions count as abnormal termination.

** `:terminates_globally`

The `:terminates_globally` settings asserts that this method will eventually terminate
(either normally or abnormally), i.e. does not infinite loop.

Note: The compiler will consider this a strong indication that the method will
      terminate relatively *quickly* and may (if otherwise legal), call this
      method at compile time. I.e. it is a bad idea to annotate this setting
      on a method that *technically*, but not *practically*, terminates.

Note: The `terminates_globally` assertion, covers any other methods called by
      the annotated method.

** `:terminates_locally`

The `:terminates_locally` setting is like `:terminates_globally`, except that it only
applies to syntactic control flow *within* the annotated method. It is this
a much weaker (and thus safer) assertion that allows for the possibility of
non-termination if the method calls some other method that does not terminate.

Note: `terminates_globally` implies `terminates_locally`.

* `:total`

The `setting` combines the following other assertions:
    - `:idempotent`
    - `:effect_free`
    - `:nothrow`
    - `:terminates_globally`
and is a convenient shortcut.

Note: `@assume_effects :total` is similar to `@Base.pure` with the primary
      distinction that the idempotency requirement applies world-age wise rather
      than globally as described above. However, in particular, a method annotated
      `@Base.pure` is always total.
```

* Changes to data structures

- Each CodeInstance gains two sets of four flags corresponding to the notions above (except terminates_locally, which is just a type inference flag). One set of flags tracks IPO-valid information (as determined by inference), the other set of flags tracks optimizer-valid information (as determined after optimization). Otherwise they have identical semantics.

- Method and CodeInfo each gain 5 bit flags corresponding 1:1 to the purity notions defined above. No separate distinction is made between IPO valid and optimizer valid flags here. We might in the future want such a distinction, but I'm hoping to get away without it for now, since the IPO-vs-optimizer distinction is a bit subtle and I don't really want to expose that to the user.

- `:foreigncall` gains an extra argument (after `cconv`) to describe the effects of the call.

* Algorithm

Relatively straightforward.
- Every call or builtin accumulates its effect information into the current frame.
- Finding an effect (throw/global side effect/non-idempotenct, etc.) taints the entire frame. Idempotency is technically a dataflow property, but that is not modeled here and any non-idempotent intrinsic will taint the idempotency flag, even if it does not contribute to the return value. I don't think that's a huge problem in practice, because currently we only use idempotency if effect-free is also set and in effect-free functions you'd generally expect every statement to contribute to the return value.
- Any backedge taints the termination effect, as does any recursion
- Unknown statements (generic calls, things I haven't gotten around to) taint all effects

* Make INV_2PI a tuple

Without this, the compiler cannot assume that the range reduction
is idempotent to make use of the new fast constprop code path.
In the future this could potentially be an ImmutableArray, but
since this is relatively small, a tuple is probably fine.

* Evalute :total function in the proper world

* Finish effects implementation for ccall

* Add missing `esc`

* Actually make use of terminates_locally override

* Mark ^(x::Float64, n::Integer) as locally terminating

* Shove effects into calling convention field

* Make inbounds taint consistency

Inbounds and `--check-bounds=no` basically make the assertion:
If this is dynamically reached during exceution then the index
will be inbounds. However, effects on a function are a stronger
statement. In particular, for *any* input values (not just the
dynamically reached ones), the effects need to hold. This is
in particular true, because inference can run functions that
are dynamically dead, e.g.

```
if unknown_bool_return() # false at runtime, but inference doesn't know
    x = sin(1.0)
end
```

Inference will (and we want it to) run the `sin(1.0)` even
though it is not dynamically reached.

For the moment, make any use of `--check-bounds=no` or `@inbounds`
taint the consistency effect, which is semantically meaningful and
prevents inference from running the function. In the future, we
may want more precise tracking of inbounds that would let us
recover some precision here.

* Allow constprop to refine effects

* Properly taint unknown call in apply

* Add NEWS and doc anchor

* Correct effect modeling for arraysize

* Address Shuhei's review

* Fix regression on inference time benchmark

The issue wasn't actually the changes here, they just added additional
error paths which bridged inference into the Base printing code, which
as usual takes a fairly long time to infer. Add some judicious barriers
and nospecialize statements to bring inference time back down.

* refine docstrings of `@assume_effects`

This commit tries to render the docstring of `@assume_effects` within
Documenter.jl-generated HTML:
- render bullet points
- codify the names of settings
- use math syntax
- use note admonitions

* improve effect analysis on allocation

Improves `:nothrow` assertion for mutable allocations.
Also adds missing `IR_FLAG_EFFECT_FREE` flagging for non-inlined callees
in `handle_single_case!` so that we can do more dead code elimination.

* address some reviews

* Address Jameson's review feedback

* Fix tests - address rebase issues

Co-authored-by: Shuhei Kadowaki <aviatesk@gmail.com>
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this pull request Feb 17, 2022
This commit consists of minor follow up tweaks for JuliaLang#43852:
- inlining: use `ConstResult` if available
- refactor tests
- simplify `CodeInstance` constructor signature
- tweak `concrete_eval_const_proven_total_or_error` signature for JET integration
Comment on lines +1760 to +1764
return Effects(Effects(), effect_free=ALWAYS_TRUE, terminates=ALWAYS_TRUE)
end
s = widenconst(argtypes[2])
if isType(s) || !isa(s, DataType) || isabstracttype(s)
return Effects(Effects(), effect_free=ALWAYS_TRUE, terminates=ALWAYS_TRUE)
Copy link
Copy Markdown
Member

@simeonschaub simeonschaub Feb 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Keno Isn't the effect_free here wrong if the first argument is a module?

julia> getfield(Main, :+)
+ (generic function with 206 methods)

julia> a + b = 1
ERROR: error in method definition: function Base.+ must be explicitly imported to be extended
Stacktrace:
 [1] top-level scope
   @ none:0
 [2] top-level scope
   @ REPL[2]:1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't consider binding resolution to be a formal effect, currently anyways

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we definitely should, since having code suddenly error just because a function got compiled seems bad.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, wait, that would mean we couldn't constant fold any bindings owned by other modules if they haven't been accessed before.

LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
…out (JuliaLang#43907)

* ldexp: Break inference loop

We have an inference loop fma_emulated -> ldexp -> ^(::Float64, ::Int) -> fma -> fma_emulated.
The arguments to `^` are constant, so constprop will figure it out, but it does require a bunch
of extra processing. There is a simpler way to write this using elementary bit operations.
Since resolving the inference loop requires constprop, this was breaking JuliaLang#43852. That is
fixable, but I think we should also make this change to avoid having an unnecessary inference
loop in our basic math functions, which will make future analyses easier.

* Make fma_emulated easier for the compiler to reason about

The fact that the `exponent` call in `fma_emulated` requires reasoning
about the ranges of the floating point values in question, which the
compiler is not capable of doing (and is unlikely to ever do automatically).
Thus, in order for the compiler to know that `fma_emulated` (and
by extension `fma`) is :nothrow in a post-JuliaLang#43852 world, create a
separate version of the `exponent` function that assumes its precondition.
We could use `@assume_effects` instead, but this version is currently
slightly easier on the compiler.

* pow: Make integer vs float branch obvious to constprop

The integer branch is nothrow, so if the caller does something like
`^(x::Float64, 2.0)`, we'd like to discover that.
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
In JuliaLang#43852 we noticed that the compiler is getting good enough to
completely DCE a number of our benchmarks. We need to add some sort
of mechanism to prevent the compiler from doing so. This adds just
such an intrinsic. The intrinsic itself doesn't do anything, but
it is considered effectful by our optimizer, preventing it from
being DCE'd. At the LLVM level, it turns into call to an external
varargs function.

The docs for the new intrinsic are as follows:
```
    donotdelete(args...)

This function prevents dead-code elimination (DCE) of itself and any arguments
passed to it, but is otherwise the lightest barrier possible. In particular,
it is not a GC safepoint, does model an observable heap effect, does not expand
to any code itself and may be re-ordered with respect to other side effects
(though the total number of executions may not change).

A useful model for this function is that it hashes all memory `reachable` from
args and escapes this information through some observable side-channel that does
not otherwise impact program behavior. Of course that's just a model. The
function does nothing and returns `nothing`.

This is intended for use in benchmarks that want to guarantee that `args` are
actually computed. (Otherwise DCE may see that the result of the benchmark is
unused and delete the entire benchmark code).

**Note**: `donotdelete` does not affect constant foloding. For example, in
          `donotdelete(1+1)`, no add instruction needs to be executed at runtime and
          the code is semantically equivalent to `donotdelete(2).`

*# Examples

function loop()
    for i = 1:1000
        # The complier must guarantee that there are 1000 program points (in the correct
       	# order) at which the value of `i` is in a register, but has otherwise
        # total control over the program.
        donotdelete(i)
    end
end
```
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
When union splitting, we currently emit a fallback block that prints
`fatal error in type inference (type bound)`. This has always been an oddity,
because there are plenty of other places in the compiler that we
rely on the correctness of inference, but it has caught a number of
issues over the years and we do still have a few issues (e.g. JuliaLang#43064)
that show this error. Nevertheless, the occurrence of issues like
this has become less frequent, so it might be time to turn it off soon.
At the same time, we have downstream users of the compiler infrastructure
that get confused by having extra `throw` calls inserted into functions
that (post-JuliaLang#43852) were inferred as `:nothrow`.

Here we add an optimization param (defaulted to on) to determine whether
or not to insert the unionsplit fallback block. Because of the conservative
default, we can decide later what the correct default is (maybe turn it on
in `debug` mode?), while letting downstream consumers play with the setting
for now to see if any issues crop up.
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
* Implement new effect system

* TLDR

Before:
```
julia> let b = Expr(:block, (:(y += sin($x)) for x in randn(1000))...)
           @eval function f_sin_perf()
               y = 0.0
               $b
               y
           end
       end
f_sin_perf (generic function with 1 method)

julia> @time @code_typed f_sin_perf()
 15.707267 seconds (25.95 M allocations: 1.491 GiB, 3.30% gc time)
[lots of junk]
```

After:
```
julia> @time @code_typed f_sin_perf()
  0.016818 seconds (187.35 k allocations: 7.901 MiB, 99.73% compilation time)
CodeInfo(
1 ─     return 27.639138714768546
) => Float64
```

so roughly a 1000x improvement in compile time performance for const-prop heavy functions.

There are also run time improvements for functions that have patterns like:
```
function some_function_to_big_to_be_inlined_but_pure(x)
....
end

function foo(x)
some_function_to_big_to_be_inlined_but_pure(x)
return x
end
```

The inliner will now be able to see that some_function_to_big_to_be_inlined_but_pure is effect free, even without inlining it and just delete it, improving runtime performance (if some_function_to_big_to_be_inlined_but_pure is small enough to be inlined, there is a small compile time throughput win, by being able to delete it without inlining, but that's a smaller gain than the compile time gain above).

* Motivation / Overview

There are two motivations for this work. The first is the above mentioned improvement in compiler performance for const-prop heavy functions. This comes up a fair bit in various Modeling & Simulation codes we have where Julia code is often auto-generated from some combination of parameterized model codes and data. This ends up creating enormous functions with significant need for constant propagation (~50k statements with ~20k constant calls are not uncommon). Our current compiler was designed for people occasionally throwing a `sqrt(2)` or something in a function, not 20k of them, so performance is quite bad.

The second motivation is to have finer grained control over our purity modeling. We have `@Base.pure`, but that has somewhat nebulous semantics and is quite a big hammer that is not appropriate in most situations.

These may seem like orthogonal concerns at first, but they are not. The compile time issues fundamentally stem from us running constant propagation in inference's abstract interpreter. However, for simple, pure functions, that is entirely unnecessary, because we have a super-fast, JIT compiler version of that function just laying around in general. The issue is that we currently, we generally do not know when it is legal to run the JIT-compiled version of the function and when we need to abstractly interpret it. However, if the compiler were able to figure out an appropriate notion of purity, it could start doing that (which is what it does now for `@Base.pure` functions).

This PR adds that kind of notion of purity, converges it along with type information during inference and then makes use of it to speed up evaluation of constant propagation (where it is legal to do so), as well as improving the inliner.

* The new purity notions

The new purity model consists of four different kinds flags per code instance. For builtins and intrinsics the existing effect free and nothrow models are re-used. There is also a new macro `@Base.assume_effects` available, which can set the purity base case for methods or `:foreigncall`s. Here is the docstring for that macro, which also explains the semantics of the new purity flags:

```
    @assume_effects setting... ex
    @assume_effects(setting..., ex)

`@assume_effects` overrides the compiler's effect modeling for the given method.
`ex` must be a method definition.

WARNING: Improper use of this macro causes undefined behavior (including crashes,
incorrect answers, or other hard to track bugs). Use with care an only if absolutely
required.

In general, each `setting` value makes an assertion about the behavior of the
function, without requiring the compiler to prove that this behavior is indeed
true. These assertions are made for all world ages. It is thus advisable to limit
the use of generic functions that may later be extended to invalidate the
assumption (which would cause undefined behavior).

The following `settings` are supported.
** `:idempotent`

The `:idempotent` setting asserts that for egal inputs:
    - The manner of termination (return value, exception, non-termination) will always be the same.
    - If the method returns, the results will always be egal.

Note: This in particular implies that the return value of the method must be
      immutable. Multiple allocations of mutable objects (even with identical
      contents) are not egal.

Note: The idempotency assertion is made world-arge wise. More formally, write
      fₐ for the evaluation of `f` in world-age `a`, then we require:

          ∀ a, x, y: x === y → fₐ(x) === fₐ(y)

      However, for two world ages `a, b` s.t. `a != b`, we may have `fₐ(x) !== fₐ(y)``

Note: A further implication is that idempontent functions may not make their
      return value dependent on the state of the heap or any other global state
      that is not constant for a given world age.

Note: The idempontency includes all legal rewrites performed by the optimizizer.
      For example, floating-point fastmath operations are not considered idempotent,
      because the optimizer may rewrite them causing the output to not be idempotent,
      even for the same world age (e.g. because one ran in the interpreter, while
      the other was optimized).

** `:effect_free`

The `:effect_free` setting asserts that the method is free of externally semantically
visible side effects. The following is an incomplete list of externally semantically
visible side effects:

 - Changing the value of a global variable.
 - Mutating the heap (e.g. an array or mutable value), except as noted below
 - Changing the method table (e.g. through calls to eval)
 - File/Network/etc. I/O
 - Task switching

However, the following are explicitly not semantically visible, even if they
may be observable:

 - Memory allocations (both mutable and immutable)
 - Elapsed time
 - Garbage collection
 - Heap mutations of objects whose lifetime does not exceed the method (i.e.
   were allocated in the method and do not escape).
 - The returned value (which is externally visible, but not a side effect)

The rule of thumb here is that an externally visible side effect is anything
that would affect the execution of the remainder of the program if the function
were not executed.

Note: The effect free assertion is made both for the method itself and any code
      that is executed by the method. Keep in mind that the assertion must be
      valid for all world ages and limit use of this assertion accordingly.

** `:nothrow`

The `:nothrow` settings asserts that this method does not terminate abnormally
(i.e. will either always return a value or never return).

Note: It is permissible for :nothrow annotated methods to make use of exception
      handling internally as long as the exception is not rethrown out of the
      method itself.

Note: MethodErrors and similar exceptions count as abnormal termination.

** `:terminates_globally`

The `:terminates_globally` settings asserts that this method will eventually terminate
(either normally or abnormally), i.e. does not infinite loop.

Note: The compiler will consider this a strong indication that the method will
      terminate relatively *quickly* and may (if otherwise legal), call this
      method at compile time. I.e. it is a bad idea to annotate this setting
      on a method that *technically*, but not *practically*, terminates.

Note: The `terminates_globally` assertion, covers any other methods called by
      the annotated method.

** `:terminates_locally`

The `:terminates_locally` setting is like `:terminates_globally`, except that it only
applies to syntactic control flow *within* the annotated method. It is this
a much weaker (and thus safer) assertion that allows for the possibility of
non-termination if the method calls some other method that does not terminate.

Note: `terminates_globally` implies `terminates_locally`.

* `:total`

The `setting` combines the following other assertions:
    - `:idempotent`
    - `:effect_free`
    - `:nothrow`
    - `:terminates_globally`
and is a convenient shortcut.

Note: `@assume_effects :total` is similar to `@Base.pure` with the primary
      distinction that the idempotency requirement applies world-age wise rather
      than globally as described above. However, in particular, a method annotated
      `@Base.pure` is always total.
```

* Changes to data structures

- Each CodeInstance gains two sets of four flags corresponding to the notions above (except terminates_locally, which is just a type inference flag). One set of flags tracks IPO-valid information (as determined by inference), the other set of flags tracks optimizer-valid information (as determined after optimization). Otherwise they have identical semantics.

- Method and CodeInfo each gain 5 bit flags corresponding 1:1 to the purity notions defined above. No separate distinction is made between IPO valid and optimizer valid flags here. We might in the future want such a distinction, but I'm hoping to get away without it for now, since the IPO-vs-optimizer distinction is a bit subtle and I don't really want to expose that to the user.

- `:foreigncall` gains an extra argument (after `cconv`) to describe the effects of the call.

* Algorithm

Relatively straightforward.
- Every call or builtin accumulates its effect information into the current frame.
- Finding an effect (throw/global side effect/non-idempotenct, etc.) taints the entire frame. Idempotency is technically a dataflow property, but that is not modeled here and any non-idempotent intrinsic will taint the idempotency flag, even if it does not contribute to the return value. I don't think that's a huge problem in practice, because currently we only use idempotency if effect-free is also set and in effect-free functions you'd generally expect every statement to contribute to the return value.
- Any backedge taints the termination effect, as does any recursion
- Unknown statements (generic calls, things I haven't gotten around to) taint all effects

* Make INV_2PI a tuple

Without this, the compiler cannot assume that the range reduction
is idempotent to make use of the new fast constprop code path.
In the future this could potentially be an ImmutableArray, but
since this is relatively small, a tuple is probably fine.

* Evalute :total function in the proper world

* Finish effects implementation for ccall

* Add missing `esc`

* Actually make use of terminates_locally override

* Mark ^(x::Float64, n::Integer) as locally terminating

* Shove effects into calling convention field

* Make inbounds taint consistency

Inbounds and `--check-bounds=no` basically make the assertion:
If this is dynamically reached during exceution then the index
will be inbounds. However, effects on a function are a stronger
statement. In particular, for *any* input values (not just the
dynamically reached ones), the effects need to hold. This is
in particular true, because inference can run functions that
are dynamically dead, e.g.

```
if unknown_bool_return() # false at runtime, but inference doesn't know
    x = sin(1.0)
end
```

Inference will (and we want it to) run the `sin(1.0)` even
though it is not dynamically reached.

For the moment, make any use of `--check-bounds=no` or `@inbounds`
taint the consistency effect, which is semantically meaningful and
prevents inference from running the function. In the future, we
may want more precise tracking of inbounds that would let us
recover some precision here.

* Allow constprop to refine effects

* Properly taint unknown call in apply

* Add NEWS and doc anchor

* Correct effect modeling for arraysize

* Address Shuhei's review

* Fix regression on inference time benchmark

The issue wasn't actually the changes here, they just added additional
error paths which bridged inference into the Base printing code, which
as usual takes a fairly long time to infer. Add some judicious barriers
and nospecialize statements to bring inference time back down.

* refine docstrings of `@assume_effects`

This commit tries to render the docstring of `@assume_effects` within
Documenter.jl-generated HTML:
- render bullet points
- codify the names of settings
- use math syntax
- use note admonitions

* improve effect analysis on allocation

Improves `:nothrow` assertion for mutable allocations.
Also adds missing `IR_FLAG_EFFECT_FREE` flagging for non-inlined callees
in `handle_single_case!` so that we can do more dead code elimination.

* address some reviews

* Address Jameson's review feedback

* Fix tests - address rebase issues

Co-authored-by: Shuhei Kadowaki <aviatesk@gmail.com>
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
This commit consists of minor follow up tweaks for JuliaLang#43852:
- inlining: use `ConstResult` if available
- refactor tests
- simplify `CodeInstance` constructor signature
- tweak `concrete_eval_const_proven_total_or_error` signature for JET integration
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Mar 8, 2022
…out (JuliaLang#43907)

* ldexp: Break inference loop

We have an inference loop fma_emulated -> ldexp -> ^(::Float64, ::Int) -> fma -> fma_emulated.
The arguments to `^` are constant, so constprop will figure it out, but it does require a bunch
of extra processing. There is a simpler way to write this using elementary bit operations.
Since resolving the inference loop requires constprop, this was breaking JuliaLang#43852. That is
fixable, but I think we should also make this change to avoid having an unnecessary inference
loop in our basic math functions, which will make future analyses easier.

* Make fma_emulated easier for the compiler to reason about

The fact that the `exponent` call in `fma_emulated` requires reasoning
about the ranges of the floating point values in question, which the
compiler is not capable of doing (and is unlikely to ever do automatically).
Thus, in order for the compiler to know that `fma_emulated` (and
by extension `fma`) is :nothrow in a post-JuliaLang#43852 world, create a
separate version of the `exponent` function that assumes its precondition.
We could use `@assume_effects` instead, but this version is currently
slightly easier on the compiler.

* pow: Make integer vs float branch obvious to constprop

The integer branch is nothrow, so if the caller does something like
`^(x::Float64, 2.0)`, we'd like to discover that.
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Mar 8, 2022
In JuliaLang#43852 we noticed that the compiler is getting good enough to
completely DCE a number of our benchmarks. We need to add some sort
of mechanism to prevent the compiler from doing so. This adds just
such an intrinsic. The intrinsic itself doesn't do anything, but
it is considered effectful by our optimizer, preventing it from
being DCE'd. At the LLVM level, it turns into call to an external
varargs function.

The docs for the new intrinsic are as follows:
```
    donotdelete(args...)

This function prevents dead-code elimination (DCE) of itself and any arguments
passed to it, but is otherwise the lightest barrier possible. In particular,
it is not a GC safepoint, does model an observable heap effect, does not expand
to any code itself and may be re-ordered with respect to other side effects
(though the total number of executions may not change).

A useful model for this function is that it hashes all memory `reachable` from
args and escapes this information through some observable side-channel that does
not otherwise impact program behavior. Of course that's just a model. The
function does nothing and returns `nothing`.

This is intended for use in benchmarks that want to guarantee that `args` are
actually computed. (Otherwise DCE may see that the result of the benchmark is
unused and delete the entire benchmark code).

**Note**: `donotdelete` does not affect constant foloding. For example, in
          `donotdelete(1+1)`, no add instruction needs to be executed at runtime and
          the code is semantically equivalent to `donotdelete(2).`

*# Examples

function loop()
    for i = 1:1000
        # The complier must guarantee that there are 1000 program points (in the correct
       	# order) at which the value of `i` is in a register, but has otherwise
        # total control over the program.
        donotdelete(i)
    end
end
```
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Mar 8, 2022
When union splitting, we currently emit a fallback block that prints
`fatal error in type inference (type bound)`. This has always been an oddity,
because there are plenty of other places in the compiler that we
rely on the correctness of inference, but it has caught a number of
issues over the years and we do still have a few issues (e.g. JuliaLang#43064)
that show this error. Nevertheless, the occurrence of issues like
this has become less frequent, so it might be time to turn it off soon.
At the same time, we have downstream users of the compiler infrastructure
that get confused by having extra `throw` calls inserted into functions
that (post-JuliaLang#43852) were inferred as `:nothrow`.

Here we add an optimization param (defaulted to on) to determine whether
or not to insert the unionsplit fallback block. Because of the conservative
default, we can decide later what the correct default is (maybe turn it on
in `debug` mode?), while letting downstream consumers play with the setting
for now to see if any issues crop up.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

latency Latency needs nanosoldier run This PR should have benchmarks run on it performance Must go faster

Projects

None yet

Development

Successfully merging this pull request may close these issues.