Overkilling the 8-queens Problem
davidad.github.io
davidad.github.io
https://github.com/awslabs/route53-infima#shuffleshard
The solution we included in Infima is a recursive search with backtracking because of some other needs; http://git.io/332VLA
but it's the N-queens problem, in a real practical setting.To get a rough baseline for what a generic solution could take, I took nqueens in Answer-Set Programming [1], set n=8, and ran it through clingo [2] on an Amazon micro instance, which gets me 15ms.
Another interesting question might be whether the two solutions have different scaling properties as n increases. The ASP solution scales like this (averaging a few runs and rounding):
n time
8 15ms
10 19
15 50
20 130
25 290
30 600
35 1050ms [1.05s]
[1] http://www.hakank.org/answer_set_programming/nqueens.lp. This encoding can be optimized a bit as well, but is a decent starting point.[2] http://potassco.sourceforge.net, a menagerie of logic-programming tools, written in C++. 'gringo' takes a high-level encoding and turns it into something more SAT-like, 'clasp' solves the SAT-like (variable-free) programs, and 'clingo' staples the two steps together into one binary. I used the version in Debian stable package 'gringo', which reports itself as "clingo 3.0.4 (clasp 1.3.10)"
I tested the standard Gecode (http://www.gecode.org) implementation from version 4.2.1 without any special options on a MacBook Pro with a 2.6GHz Intel Core i7. Note that Gecode does not have bit-set representations for domains, so it is not optimized for this special case. Tests were run for 500 iterations and repeated 5 times to get deviations below 2.5%. I get the following timings for your sizes plus 50 and 100:
n time
8 0.0368 ms
10 0.0243 ms
15 0.0221 ms
20 0.1263 ms
25 0.2677 ms
30 0.1621 ms
35 0.2433 ms
50 2.3234 ms
100 0.5509 ms
The timings are of course all strongly dependent on the heuristic used for finding the solution which is shown by the varying times above.Constraint programming is typically also very dependent on the the pruning power of the constraints. Adding the option "-icl dom" which makes the pruning for the all-different constraints domain consistent, gives the following timings.
n time
8 0.1138 ms
10 0.0603 ms
15 0.0879 ms
20 0.2801 ms
25 0.6612 ms
30 0.4657 ms
35 0.7553 ms
50 2.7687 ms
100 8.7687 ms
As can be seen, domain consistency did not actually help us in this case time-wise. The number of search-nodes visited range between 17 and 1066 for the normal consistency level, while the number of nodes visited for the domain consistent version range between 14 and 286. The reduction in search tree size was however offset by the increased complexity of the propagator. n time search tree solutions
8 0.8752 ms 767 92
10 14.2759 ms 11431 724
12 306.7616 ms 232163 14200
14 8782.4620 ms 6391931 365596
Downloading the c-versions from https://github.com/davidad/8queens/tree/%2Bc_comparison gives a reference-time using the time built-in command of around 750 ms for 10k iterations. This means that the C solver takes around 0.075 ms per iteration which is a factor of ten faster than the Gecode version. That is not unreasonable given that the Gecode version is a high-level model in a general framework.I did not manage to build the assembly-version unfortunately.
EDIT: Updated the timings and comments of the C-solver to represent that it actually solved the problem 10k times. Lesson, never do benchmarks quickly :-)
I was well and truly pwned, as they say (but at least I got some sleep last night).
BTW, the convention would be for `static SIZE: i8 = 8;` (i.e. upper case), and I would strongly recommend you don't use `i8` for that, because SIZE * SIZE = 0 (but not in const-exprs, it seems). `uint` is the type of the smallest size that is guaranteed that to work for all array indexing.
Also, you can use `bool` instead of i8 for your array elements. (A Rust bool is a u8/i8 that is guaranteed to be either 0 or 1, i.e. exactly what your code has.)
Lastly, one conventionally matches on just `None` rather than `None()`. (This actually lead me to file https://github.com/mozilla/rust/issues/12560 to make it require that there be no `()`, so thank you for prompting that.)
As an aside there seems to be lots of really interesting concepts involved involved in implementing chess engines.
From representing the chessboard in a highly compact manor through the use of bitboards (http://www.frayn.net/beowulf/theory.html#bitboards) to searching for 'best' possible moves.
windows.h is for QueryPerformanceCounter used only in main(); compile with gcc -Ofast
It iterates/counts all solutions + prints first of them (as 01 board) in 0.23ms on my i7 (if you redirect output to file). You can also run it for N queens changing #define Qs to something bigger. The code as you see is silly (not needed conditional in main loop among other things) and not optimized, still it's pretty fast :) I am not sure what hardware they used but it takes 10x less time on my computer vs theirs.
bash-3.2$ make
gcc -O3 8q_C_bluecalm.c -o 8q_C_bluecalm
nasm 8q_x64_davidad.asm -DLOOPED=10000 -f macho64 -o 8q_x64_davidad.o
ld -o 8q_x64_davidad 8q_x64_davidad.o
time ./8q_C_bluecalm ; echo $?
real 0m0.802s
user 0m0.801s
sys 0m0.001s
92
time ./8q_x64_davidad ; echo $?
real 0m0.113s
user 0m0.112s
sys 0m0.000s
92
If you'd like, I'm happy to post this as a branch of my repository so others can replicate. (I only hesitate because it's a derivative work of your code.)It will be nice addition to see how hand tuned code beats naive C implementation. (I hope your code iterates over all solutions as well, the easiest way to change mine to stop after 1st is a goto from solve to main :))
EDIT: Yeah, I meant if it counts all of them and I've just noticed 92 exit code :) I didn't expect that big a speed-up, impressive!
EDIT2: to answer children post: That's great, please change -O3 to -Ofast though if you are using new GCC. It shouldn't matter much but it sometimes does; I believe -march=native won't help with generated assembly though I seems that your code from original article spent all its time in pretty printing. I am happy to see some real comparison now; I am for sure learning a lot here once I understand everything in the article.
[1] https://github.com/davidad/8queens/tree/%2Bc_comparison
EDIT: I don't seem to have -Ofast, but I added a note to the readme to ask people to try it. I also added the recommended flags for Sandy Bridge, i.e. -march=core2 -msse4.1 -msse4.2.
$ make
gcc -O3 -march=core2 -msse4.1 -msse4.2 8q_C_bluecalm.c -o 8q_C_bluecalm
time ./8q_C_bluecalm ; echo $?
real 0m0.376s
user 0m0.373s
sys 0m0.001s
92
time ./8q_x64_davidad ; echo $?
real 0m0.129s
user 0m0.128s
sys 0m0.001s
92
Of course this doesn't matter if the point is to compare the best asm dfs to the best C dfs of the same space, but if the point is to make the best asm 8queens solver, you can get down below 6μs like this :)Edit: Using C++ templates to inline the DFS also helps
time ./8q_C_bluecalm ; echo $?
real 0m0.260s
user 0m0.258s
sys 0m0.001s
92
Unfortunately this sort of technique doesn't really apply to the asm >_>Edit again: I tried to go a bit further by replacing the board struct with your method of passing 3 bytes between levels of the dfs, but at that point the compiler was able to optimize out the whole program.
Removing the templates causes the compiler to actually emit a dfs, which by now is faster than the asm (!). Removing the 2x speedup from just not looking at half of the tree slows us back down to ~1.1x the runtime of the asm:
time ./8q_C_bluecalm ; echo $?
real 0m0.140s
user 0m0.138s
sys 0m0.001s
92
Here's this last solution, the one that's ~1.1x slower than the asm on my machine: http://pastie.org/8784768And with the free 2x speedup: http://pastie.org/8784770
One last time now: I tried getting rid of the x arg and using a global. This was much slower, so I tried getting rid of the SOLUTIONS global and using a return value. This caused the compiler to optimize out the whole program again :(
I implemented your "free 2x speedup" in asm [1]. Sure enough, solution time improved to 4.6us on my machine [2].
[1] https://github.com/davidad/8queens/commit/61119a7f0019e4a85e... [2] https://github.com/davidad/8queens/tree/free-2x-speedup
My immediate question was whether another compiler might be more competitive with the assembly. On my Sandy Bridge processor, I found that although GCC was the slowest, the assembly was still the clear winner:
gcc 4.8.0: 0.801596113 seconds
icc 14.0.1: 0.739297534 seconds
clang 3.2: 0.706446818 seconds
assembly: 0.104038212 seconds
I was surprised by Clang here. It's an older version than the other two, yet fastest. A quick glance at 'perf stat' (which I used for the timing) says that although it's executing fewer instructions per cycle, it's managing to use fewer instructions than the other two.Although it doesn't seem to make much of a difference here, those are odd flags for Sandy Bridge. Core2 is a previous generation, and it supports AVX which came out after SSE4.2. If you did to specify SB, you'd want the unwieldy "-march=corei7-avx". But probably better just to use '-mavx' or 'march=native', along with -Ofast or -O3.
If redirected to file it takes 0.232ms, again to iterate all the solutions and print first of them.
It would be nice if authors provide some way to compare it to their code. Preferably on say 15 queens instead of 8 so pretty printing time could be ignored :)
If you're excited about it, though, I'd encourage you to give it a shot and write a blog post about your experience! However hard it turns out to be, I bet we'd all learn something new. (:
That said, the size optimiser in me sees a noticeable absence of rbx, rsi, rdi, and ebp; those registers should be used before going into the r8~r15, since the "extended" registers require a prefix byte to the instruction every time they're used. In fact since only r8, r10, r13, r14, and r15 are used, this code could be rewritten to use only one extended register and you wouldn't need the "mov rdi, r8" at the end.
It's hard to argue against "shorter", but since the modern processors this code targets all have a decoded instruction micro-op cache, there is no front-end bottleneck and the instruction length doesn't actually slow anything down.
Also, the four registers you mention are all callee-save on Windows (just RBX and RBP for Mac and Linux) , so avoiding them means you don't have to save and restore them. Given a choice, use a scratch register. That said, R12-R15 are callee-save too, so this may not apply here.
Personally, I dislike the messiness of the historical names when using them for non-historical purposes. I think if it was more acceptable to alias them to R1, R2, etc I'd prefer them more than I do.
Right, this code is running entirely in the core and that's part of what makes it so fast.
Also, the four registers you mention are all callee-save on Windows (just RBX and RBP for Mac and Linux) , so avoiding them means you don't have to save and restore them.
Callee-save and caller-save are just a calling convention; and one of the advantages of using Asm is you don't have to care about calling conventions except if you're interfacing with some other language, which isn't the case here - it's pure Asm. There's not even a single function call in it.
Besides, saving and restoring those registers (if you really need to) only takes 4 bytes each (2 in 32-bit mode) - a push and a pop. This tradeoff pays off if you're going to use them in more than 4 instructions.
UPDATE: If I allow javascript the web page is rendered using some google fonts which probably do not provide any ligatures. However if I do not allow JS and Iceweasel uses my default serif font (Minion Pro) the page is littered with all sorts of contextual/historic/stylistic ligatures.
Examples: https://imgur.com/a/7Fcdm
I've been doing some genealogy research recently (caution: if you are obsessive this can become a real time suck), some of this has involved reading old German and Austrian newspapers, A typical obituary page might have Swiss, Roman and Gothic (black letter) faces, in particular the gothic tends to make extensive use of ligatures
I experienced this first hand as a child - more summer vacations than I'd have cared for involved trips arranged so we'd pass by various cemetery's or churches so my dad could check graves or church books against his latest leads...
div {
text-rendering: optimizeLegibility;
font-variant-ligatures: common-ligatures;
-webkit-font-variant-ligatures: common-ligatures;
-moz-font-feature-settings: "liga", "dlig";
-ms-font-feature-settings: "liga", "dlig";
-webkit-font-feature-settings: "liga", "dlig";
-o-font-feature-settings: "liga", "dlig";
font-feature-settings: "liga", "dlig";
}
The text-rendering style enabled optional ligatures. The font-variant-ligatures style enables the use of OpenType "liga" and "clig" ligatures, but then the font-feature-settings style overrides that with "liga" and "dlig" (discretionary ligatures). What that means depends on how your font designer flagged their various ligatures, of course, but the page is explicitly opting into more than just common ligatures.This is going to sound crazy: Does anyone know the name of the js/css development website that has three or four panes and lets you mock up things on screen. There is probably more than one. I can not think of the name right now for the life of me. I always see it linked to when people are doing demonstrations here on HN.
I put up an image here of your defaults with your fonts and mine:
My default font is Minion Pro and you can see the extra discretionaries in words like "spurred, directed, testing, etc." The one goofy css test that I found did not let me choose any web fonts so I could not check your garamond but I did check my local Garamond Pro. and all of the same discretionary ligatures are present in my local garamond.
its in ~210 lines of C++:
https://bitbucket.org/rfc/dopt_nqueen/src/3e812164d97a8e91d4...
uses random local search approach, as described in russell and norvig (http://aima.cs.berkeley.edu/).
wrote this code during https://www.coursera.org/course/optimization
That'll do ~N=1e6 in a second (I'm sure there are lots of optimisations that could be done in it though), or solutions for N=4 to N=1000 in less than 10.
The more complex problem is finding out how many solutions there are in total.
That'll do ~N=1e6 in a second (I'm sure there are lots of optimisations that could be done in it though), or solutions for N=4 to N=1000 in less than 10.
The more complex problem is finding out how many solutions there are in total.
Edit: Turns out he created it after I graduated. See here: http://www.cl.cam.ac.uk/~mr10/MCPL.html for more info