-
-
Notifications
You must be signed in to change notification settings - Fork 21.3k
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
Fix SortArray crashing with bad comparison functions #15536
Conversation
b357d94
to
d587a7e
Compare
I don't really think it's a problem that it crashes, though the fix does not really seem to affect performance much. I prefer we merge this after 3.0 as it's not urgent |
This should be a debug-only thing, right? Just to inform naive developers that the sort function must be consistent. |
I'm not sure. I'm not against having it in production as well, as the overhead is one additional register compare per iteration without memory access, which will be very hard to measure. While active, it can catch subtle errors in a comparator that might not crash otherwise but just produces wrong sort results (it can make it fail earlier than the crash). Godot has a lot of |
I think it should go in production to avoid people's exported games crashing. |
@Zireael07 That was my idea as well. |
I think I would do the following with this PR. Truth is that in some places this code is very performance critical (renderer), so the extra check is undesired. But it is true that for the case you mention (GDScript) it can be useful:
|
And of course, enabling it always if DEBUG_ENABLED is true |
07ddbbe
to
a2f2bf3
Compare
Changed accordingly via a new template parameter I would actually be surprised if this had a performance impact on desktop CPUs, as it's just one unlikely compare of two values that are already in registers, and they should happen for free while waiting for the memory read of the next loop iteration's compare values. Then again, I'm not familiar with mobile CPUs and cross compilers to webasm, and who knows what happens there. |
Fixes #3327
Fixes #10968
In master,
SortArray
crashes if the comparison function is not good (i.e. not totally ordered). Note that a comparator is already bad if it implements<=
instead of<
.This PR prevents crashing with bad comparators and outputs nagging error messages instead (quite a lot, you won't miss them).
I ran an obligatory stress test against the sorter with good comparators for about 30 minutes, indicating that neither did the usual functionality got broken, nor does any of the three conditions this PR asserts as errors occur in normal operation:
DETAILS
The sorting code is complex and relies on many implicit assertions, so here is some lengthy rationale why this is correct.
There seem to be three areas where problems with a bad comparison function could happen inside
SortArray
(note that I'm using Python slice notation, i.e.x[a:b]
excludesx[b]
):(1) In
unguarded_linear_insert
.With a totally ordered comparison function,
compare(p_value, p_array[next])
will never a returntrue
here fornext == 0
.For the sake of shortness, let's just state that if it did, the code would be erroneous, as
next--
would setnext
to-1
and would next access the out-of-boundsp_array[-1]
in the while head.We can safely assume that if
compare
returnstrue
fornext == 0
, the comparator is bad here.Longer explanation: for good comparators, the condition just described (and checked for in this PR) will never happen:
linear_insert
this is achieved via checkingif (compare(val, p_array[p_first]))
before the call, thusp_value
will never be smaller than some lower index element atp_first >= 0
.unguarded_insertion_sort
there will have been a clever (bit obscure) partitioning viaintrosort
beforehand (seesort_range
) that always ensures some smaller item in the lower partition of the array (i.e. the call tounguarded_insertion_sort(p_first + INTROSORT_THRESHOLD, p_last, p_array);
can never insert an item that will be smaller than all items inp_array[p_first, p_first + INTROSORT_THRESHOLD]
).(2) In
partitioner
.In simple terms:
p_first
, going forward, must hitp_pivot
(with afalse
compare) before going beyond the end of the partition range, andp_last
, going backward, must hitp_pivot
(with afalse
compare) before going before its beginning. If all compares, including the compares on the boundaries, returntrue
, the comparator is bad.Longer explanation: first note that for all calls of
partitioner
,p_pivot
is always an element fromp_array[p_first:p_last]
(computed viamedian_of_3
).That means there must an
x
,p_first <= x < p_last
, such thatp_array[x] == p_pivot
and thuscompare(p_array[x], p_pivot) == false
.The two
ERR_BAD_COMPARE
checks added by this PR check if the condition just stated fails, thus indicating that the comparator must be bad (e.g. forp_first
this happens whenp_first
gets incremented until it reaches the original unmodifiedp_last - 1
without hitting uponp_pivot
).Note that the
p_first++;
at the end of thewhile
loop is always safe, as we know fromif (!(p_first < p_last))
before thatp_first < p_last < unmodified_last
, i.e.p_first < unmodified_last - 1
, i.e.p_first + 1 < unmodified_last
.(3) The heap functions there should be no problem.
push_heap
's loop is guarded byparent
, andadjust_heap
's loop is guarded bysecond_child
.