-
Notifications
You must be signed in to change notification settings - Fork 39
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
Shrinker improvements #235
Conversation
c7e0b16
to
4cee008
Compare
Below follows the output from a rewamped shrinker performance benchmark run of #177. In total QCheck goes from 67.071s to 8.064s - not a bad improvement.
|
56f99f1
to
96b08d7
Compare
OK, using the merged shrinker performance benchmark I've now cherry-picked each of the four shrinker improvements and measured each of them.
Executive summary:
Whereas the latter represents a statistical significant improvement (measured with qcheck/test/core/QCheck_unit_tests.ml Lines 20 to 24 in 824dafb
We should address this separately. I therefore move to merge the first three commits. Measurement detailsTo keep the tables focused below I've deleted benchmark entries with a total QCheck1 shrinker time below 0.1 seconds. Here's first the performance of current master. Note how the focused tables highlight string, list, and function benchmarks.
Step 1 Here's the performance of the simpler
Step 2 Here's the performance of using the improved The
Step 3 Here's the performance of adding the improved function shrinker on top.
Step 4 Here's the performance of adding the improved
Statistical test For reference here's the total timings of 10 runs of step 3:
and 10 runs of step 4:
and the output of
|
I will rebase this PR shortly. For reference, here's the considered (* inspired by QCheck2's int shrinker algorithm (non-exhaustive) *)
let int x yield =
let curr = ref 0 in (*to return 0 repeatedly *) (*was: let curr = ref (x/2) *)
(* try some divisors *)
while !curr <> x do
yield !curr;
let half_diff = (x - !curr)/2 in (*was: let half_diff = (x/2) - (!curr/2) in *)
if half_diff = 0
then curr := x
else curr := !curr + half_diff
done Edit: To avoid too much forcing and overwriting I'll submit a separate PR instead. |
if x<0 then yield (x+1); | ||
() | ||
while !curr <> x do | ||
yield !curr; |
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.
Unrelated to your merge request but what is yield
? As x
is continuously shrinking I guess it it's in charge of both marking the numbers of steps but also stop when the shrinker found the minimal value?
The int shrinker improvement makes sense imo. Starting at |
This PR combines QCheck(1) shrinker improvements to
list
,string
, functions, andint
.The improvements arose from studying the benchmark outputs from #177 across a range of equivalent tests.
Here it became clear that both the QCheck(1) and QCheck2 shrinkers each had some sore spots.
As a first step, I set out to get QCheck(1) up to speed with QCheck2's performance on the tests where its shrinkers were lacking behind.
I don't expect to merge the PR in the current form, partly because it includes an
int
shrinker along the lines of the somewhat controversial #173, but@vch9 : if you (or others) still have QCheck(1) tests (needing shrinking) I'd be curious to hear if you observe a difference
when pinning /~https://github.com/jmid/qcheck/tree/shrinker-improvements
Whereas these improvements have been obtained over artificial benchmark programs, I've been bitten in the past by shrinker performance in bigger developments, e.g.,
The core is an improved
list_spine
shrinker. As an added bonus this implicitly improves function shrinking.Secondly, we can reuse the improved
list
shrinker forstring
s - by breaking them up inchar list
s - and then use thechar
-shrinker to reduce the individualchar
s. Overall, these three were identified as QCheck(1) sore spots in #177.Despite the reduced complexity we are able to arrive at the same counterexamples - or simpler(!) as should
be clear from the expect-test logs.
Here's the new and simpler shrinker of list spines using recursion:
The base cases with 0,1, or 2 elements are not surprising.
In the inductive case, the
yield xs
represents dropping the last half of the list (akin to bisection). The yield from the recursive call in the last line combines a reduced front half with an untouched second halfys
.So how well does this fare? It turns out to work reasonably well. Here's an example:
As an added bonus, there are both children containing and excluding the list head. This is useful, since sometimes it is central to keep it in a counterexample and sometimes it is not.
The list of children does not grow much longer on an input list twice as long:
In comparison, here's the output from the same lines run with the current
list_spine
shrinker:Because of the repeated
chunk
reduction (each O(log n)chunk
size is attempted at a sequence of positions) the complexity of the currentlist_spine
is something like O(n log n), whereas the new one is O(log n).This is just for one run though. After a successful reduction, each shrinker is restarted.