-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid stack overflow by reducing stack usage of BinaryExpr::evaluate
in debug builds
#1047
Conversation
@@ -105,8 +105,6 @@ jobs: | |||
run: | | |||
export ARROW_TEST_DATA=$(pwd)/testing/data | |||
export PARQUET_TEST_DATA=$(pwd)/parquet-testing/data | |||
# run tests on all workspace members with default feature list + avro |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the workaround added in #910
@@ -543,86 +543,17 @@ impl PhysicalExpr for BinaryExpr { | |||
))); | |||
} | |||
|
|||
// Attempt to use special kernels if one input is scalar and the other is an array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no intended changes to this function's behavior, simply breaking it up into several smaller functions
let schema = batch.schema(); | ||
|
||
// build a left deep tree ((((a + a) + a) + a .... | ||
let tree_depth: i32 = 100; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On master, this test causes a stack overflow with tree_depth
of 10
. After the changes in this PR it passes successfully with a tree_depth of 100 (I didn't try any bigger)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the deep-dive evaluation and the fix, @alamb
let schema = batch.schema(); | ||
|
||
// build a left deep tree ((((a + a) + a) + a .... | ||
let tree_depth: i32 = 100; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
I'll plan to merge this tomorrow unless there are objections |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job on the deep dive and detailed diagram :)
This kind of "match to many arms with macros" pattern is very common in our code base. It seems like in the future if we want to get fancy, we could automate detection of such problem in our code base. For example, having a linter tool automatically find out all recursive functions and check their stack size to see if it has passed a certain threshold. |
I agree -- if we run into this problem again, investing in an automated tool like that sounds like a good idea. |
Which issue does this PR close?
Closes #419
Rationale for this change
Prior to this PR, trying to evaluate
BinaryExprs
more than a few level deep (e.g. something likea + a + a + a + a + a + a + a ...
) would result in a stack overflowFor example the test included fails like this with
tree_depth
of 10 (in debug builds)What changes are included in this PR?
BinaryExpr::evaluate
into a few smaller functionsAre there any user-facing changes?
Not really other than avoiding stack overflows while evaluating queries in debug builds
Technical Backstory
I believe the issue is that in debug builds, each local variable gets its own (unique space in the stack). Due to the size of
BinaryExpr::evaluate
(largely hidden by macros) this results in a ludicrous amount of stack required for each call toBinaryExpr::evaluate
. SinceBinaryExpr::evaluate
is implemented recursively this means even a few nesting levels exhausts a 2MB stack.You can see evidence of looking at the disassembly. Here is how I did it on a mac:
otool -vt target/debug/deps/datafusion-68280be86fef135e > /tmp/df.asm
And the associated assembly shows the stack size to be 0x55a10 (350736 bytes)
In case you were curious, the same function in a release build only uses 0x398 (920 bytes) of stack space.
For those of you visually minded, here is an illustration:
The "Fix" if you will is to break BinaryExpr into smaller functions that each require less stack space
The stack frame size after this PR is 0x770 = 1904 bytes
Note that even though some of the new functions require non trivial stack size (listed below), a major difference is they are not called recursively and thus there is only ever one frame of them on the stack: