[1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Bugs that are actually features.
mrvn
Smart Inserter
Smart Inserter
Posts: 5969
Joined: Mon Sep 05, 2016 9:10 am
Contact:

[1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by mrvn »

In my FractalMaps mod I generate an elevation function that is rather slow with many nested if_else_chain constructs. The elevation is defined recursively but looking at the result it has very regular squares. So instead of going through 11 levels of recursion before it hits a square I thought I would pull that to the top level and generate them with a simpler noise.less_than and noise.modulo expression and only do the complex recursive expression for the non trivial parts.

But somehow that just got slower. So I tried something simpler just to see how the speed changes. The original code for the recursive defintion is constructed like this:

Code: Select all

    return noise.if_else_chain(
      noise.less_than(48, abs(y)), make(t, x, depth - 1),
      noise.less_than(abs(x), 16), 1,
      noise.less_than(abs(y), 16), x,
      -1
    )
This generates a map preview in 44s. Now if I change it like this:

Code: Select all

    return noise.if_else_chain(
      1, 1,
      noise.less_than(48, abs(y)), make(t, x, depth - 1),
      noise.less_than(abs(x), 16), 1,
      noise.less_than(abs(y), 16), x,
      -1
    )
then it only takes 6s for the map preview. The noise function short circuits the evaluation and everything becomes land. 4.3s are spend on loading modules and 2s for the map preview. That's comparable to the vanilla noise function.

But if I change it like this:

Code: Select all

    return noise.if_else_chain(
      noise.less_than(x, 0), 1,
      noise.less_than(48, abs(y)), make(t, x, depth - 1),
      noise.less_than(abs(x), 16), 1,
      noise.less_than(abs(y), 16), x,
      -1
    )
Now only half the area matches the first condition. So I would expect this to take about 20-25s. But it actually takes 47s.

It looks like a "noise.less_than" expression doesn't allow the "if_else_chain" expression to short circuit and it still evaluates all the other expressions.
User avatar
boskid
Factorio Staff
Factorio Staff
Posts: 3273
Joined: Thu Dec 14, 2017 6:56 pm
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by boskid »

As far as i know about the noise expression internals (which is not much), noise programs when running are working as a SIMD, basically a compiled (to internal representation) noise program consists of many instructions and instruction works on a long vector of data: in case of a chunk, such vector will have 1024 values and all operations working on "registers" like adding will be in fact adding such long vectors to produce another vector. Given there may be some deduplication happening that reuses some of the subexpressions, they may still need to be evaluated even if not used by specific if_else branch. That means everything works as expected so far and this is a "not a bug", but i will wait with moving until someone else confirms this.
Genhis
Factorio Staff
Factorio Staff
Posts: 709
Joined: Wed Dec 24, 2014 8:19 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by Genhis »

Thanks for the report, the issue should be fixed for the 2.0 release.
mrvn
Smart Inserter
Smart Inserter
Posts: 5969
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by mrvn »

boskid wrote: ↑Wed Sep 07, 2022 7:29 am As far as i know about the noise expression internals (which is not much), noise programs when running are working as a SIMD, basically a compiled (to internal representation) noise program consists of many instructions and instruction works on a long vector of data: in case of a chunk, such vector will have 1024 values and all operations working on "registers" like adding will be in fact adding such long vectors to produce another vector. Given there may be some deduplication happening that reuses some of the subexpressions, they may still need to be evaluated even if not used by specific if_else branch. That means everything works as expected so far and this is a "not a bug", but i will wait with moving until someone else confirms this.
That sounds like they should get compiled as GPU code. But then why does a map preview use all 8 CPU cores?

Deduplication and reusing subexpressions is something you can also do manually:

Code: Select all

    local t = noise.delimit_procedure(128 * 2 ^ (math.floor(depth / 2)) - abs(y))
I found the internal compiler wasn't smart enough to lift that subexpression out of the formula. I have 11 of those (one per depth) in my noise function so maybe I was just overloading the compiler. Deduplicating that expression by hand was a major speed gain for me (factor 3 iirc). I have a lot of duplicate "abs(x)" and "abs(y)" expressions so maybe I should try deduplicating them by hand too. It stopped optimizing when it became playable.

The "make(t, x, depth - 1)" expression is the recursion and occurs nowhere else so there shouldn't be any deduplication being done there, or at least it shouldn't lift it out of the "if_else_chain". It should only evaluate the deduplicated expression if that branch is taken.
mrvn
Smart Inserter
Smart Inserter
Posts: 5969
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by mrvn »

Genhis wrote: ↑Wed Sep 07, 2022 7:46 am Thanks for the report, the issue should be fixed for the 1.2 major release.
Thanks, looking forward to it. I keep going back to playing factorio, best money I ever spend.
Bilka
Factorio Staff
Factorio Staff
Posts: 3310
Joined: Sat Aug 13, 2016 9:20 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by Bilka »

mrvn wrote: ↑Wed Sep 07, 2022 11:02 am It should only evaluate the deduplicated expression if that branch is taken.
In 1.1 if you're not working with constant values (noise.var("x") is not a constant value), every branch of the if else chain is always evaluated. At least, that's afaik how I wrote the code.

If you want to get around that, you can try to not use the if else chain. You can use that less_than returns 0 or 1 and use it as a multiplier instead of as an if condition. From my notes, most likely based on something by Maxreader: "return result * boolean + otherResult * (1-boolean)". No guarantees that this is better, but it should be slightly less naive than 1.1.x if else chain.
I'm an admin over at https://wiki.factorio.com. Feel free to contact me if there's anything wrong (or right) with it.
mrvn
Smart Inserter
Smart Inserter
Posts: 5969
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by mrvn »

Bilka wrote: ↑Wed Sep 07, 2022 1:12 pm
mrvn wrote: ↑Wed Sep 07, 2022 11:02 am It should only evaluate the deduplicated expression if that branch is taken.
In 1.1 if you're not working with constant values (noise.var("x") is not a constant value), every branch of the if else chain is always evaluated. At least, that's afaik how I wrote the code.

If you want to get around that, you can try to not use the if else chain. You can use that less_than returns 0 or 1 and use it as a multiplier instead of as an if condition. From my notes, most likely based on something by Maxreader: "return result * boolean + otherResult * (1-boolean)". No guarantees that this is better, but it should be slightly less naive than 1.1.x if else chain.
I will give it a try but I can't see that working at all, unless evaluation is operators before operands and right to left. The boolean has to be evaluate first and then the multiplication has to see "Oh, it's 0, I don't need to evaluate the left operand at all". Would surprise me a lot if that works. I mean it's probably operators before operands because that's the tree structure you create for a noise expression. But right to left? I will try "boolean * result" as well in case it only works left to right.

Trying right to left:

Code: Select all

    expression = noise.clamp(make(var_x, var_y, depth), -1, 0) * 0
46s, slight slowdown for the extra multiplication?

Trying left to right:

Well, right to left evaluation might make sense the way the operator overloading is done in noise: "0 * expr" ==> attempt to perform arithmetic on a table value. "tne(0) * expr" it is.

Code: Select all

    expression = tne(0) * noise.clamp(make(var_x, var_y, depth), -1, 0)
45s, same thing withing the margin of error.

Doesn't seem to work either direction. Will have to wait for 1.2 then.
Genhis
Factorio Staff
Factorio Staff
Posts: 709
Joined: Wed Dec 24, 2014 8:19 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by Genhis »

mrvn wrote: ↑Wed Sep 07, 2022 11:04 am
Genhis wrote: ↑Wed Sep 07, 2022 7:46 am Thanks for the report, the issue should be fixed for the 1.2 major release.
Thanks, looking forward to it. I keep going back to playing factorio, best money I ever spend.
It seems that I misread your bug report. After reading through it again, the main issue in your report is not fixed, I'm sorry. 'If' expressions with variable conditions can't short-circuit because, as boskid says, values are processed in batches of 1024 (one chunk). Furthermore, procedure operations are computed sequentially with prerequisites resolved beforehand, so 'if' can only compute its result after values of all its inner expressions are known. Therefore, this is not a bug.

You can, however, expect improved performance of 'if' conditions in 1.2. Every condition does its own memory allocation in 1.1, so the performance (excluding complexity of its arguments) scales linearly. In 1.2, conditions re-use the buffer created for each map generation thread.
mrvn
Smart Inserter
Smart Inserter
Posts: 5969
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by mrvn »

Genhis wrote: ↑Mon Sep 12, 2022 5:58 pm
mrvn wrote: ↑Wed Sep 07, 2022 11:04 am
Genhis wrote: ↑Wed Sep 07, 2022 7:46 am Thanks for the report, the issue should be fixed for the 1.2 major release.
Thanks, looking forward to it. I keep going back to playing factorio, best money I ever spend.
It seems that I misread your bug report. After reading through it again, the main issue in your report is not fixed, I'm sorry. 'If' expressions with variable conditions can't short-circuit because, as boskid says, values are processed in batches of 1024 (one chunk). Furthermore, procedure operations are computed sequentially with prerequisites resolved beforehand, so 'if' can only compute its result after values of all its inner expressions are known. Therefore, this is not a bug.

You can, however, expect improved performance of 'if' conditions in 1.2. Every condition does its own memory allocation in 1.1, so the performance (excluding complexity of its arguments) scales linearly. In 1.2, conditions re-use the buffer created for each map generation thread.
I can see how that might apply since every tile of those 1024 might pick another branch of the if-else-chain and the boolean condition is just a selector that selects the pre-computed value A or B. Bug god is that inefficient.

Could there be a special expression to compute a value for a chunk? That would then be a constant for those 1024 tiles.
Genhis
Factorio Staff
Factorio Staff
Posts: 709
Joined: Wed Dec 24, 2014 8:19 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by Genhis »

This would require a large rework of the noise program system. A noise program, which noise expressions get compiled into, has no concept of branches. All operations in a procedure are sequential and their result can be reused within the procedure if needed. It is similar to delimit_procedure deduplication system but within the same procedure, which is more efficient for standard use case.

Currently, there are no plans to add branches to this system.
mrvn
Smart Inserter
Smart Inserter
Posts: 5969
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by mrvn »

I was thinking of something like this:

Code: Select all

local constant = noise.delimit_procedure_chunk(expression_using_chunk_coordinates)
noise.if_else_chain(constant, expr1, expr2)
It would actually be already enough if "if_else_chain" checks if a variable is identical for all 1024 tiles:

Code: Select all

noise.if_else_chain_chunk(condition, expr1, expr2)
The use of "if_else_chain_chunk" is there so the identical check is only done when the programmer indicates this to be likely to help. If the condition is identical for all tiles then this goes into the same code path as if condition is a constant, if it fails then the other code path is taken. This would probably be the easier thing to implement.
Genhis
Factorio Staff
Factorio Staff
Posts: 709
Joined: Wed Dec 24, 2014 8:19 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by Genhis »

Yeah, I understand what you mean but it is not easily doable.

The only thing if-else does is deciding which of already computed values should be forwarded/returned from the expression based on the condition. At runtime stage, 'if' has hard depencendies on its arguments, as do all noise operations, i.e. it can't start its logic until all arguments are calculated. This is because all operations in a procedure are computed in sequence. They don't know their dependencies because they were already ordered correctly.

Adding branches and true conditional execution to the system could make it unnecessarily complex and would likely degrade performance for vanilla game or mods which don't use conditions on a large scale.
mrvn
Smart Inserter
Smart Inserter
Posts: 5969
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by mrvn »

Genhis wrote: ↑Tue Sep 13, 2022 10:46 am Yeah, I understand what you mean but it is not easily doable.

The only thing if-else does is deciding which of already computed values should be forwarded/returned from the expression based on the condition. At runtime stage, 'if' has hard depencendies on its arguments, as do all noise operations, i.e. it can't start its logic until all arguments are calculated. This is because all operations in a procedure are computed in sequence. They don't know their dependencies because they were already ordered correctly.

Adding branches and true conditional execution to the system could make it unnecessarily complex and would likely degrade performance for vanilla game or mods which don't use conditions on a large scale.
But when the condition is a literal then it works. Is that the compiler eliminating the "if" in that case?
Genhis
Factorio Staff
Factorio Staff
Posts: 709
Joined: Wed Dec 24, 2014 8:19 am
Contact:

Re: [1.1.68] noise.if_then_else does not short circuit when contion isn't a constant

Post by Genhis »

Yes, the compiler can eliminate one of the branches during the compilation process. It can also simplify expressions, such as addition of two constants, to improve runtime preformance as much as possible. The condition doesn't strictly have to be a literal, elimination also works with per-surface constant variables (map seed, starting positions, etc.), or anything which can be simplified into a constant at compile time.

The compilation happens when a surface is created and the resulting noise program is then used for all chunks on that surface. So, only variables which are the same for all chunks can be treated as constants.
Post Reply

Return to β€œNot a bug”