The main aim of this project is to take an existing but somewhat dated C++ solution to solving Sudoku puzzles (originally written by incumbent Singapore Prime Minister Lee Hsien Loong) and update it to use modern and idiomatic C++14. This project doesn't aim to improve or otherwise modify the underlying algorithm applied in solving the Sudoku puzzles, rather it aims to ensure that the program design adopts an object-oriented memory-safe approach, as well as to ensure the inputs/outputs are well-guarded against malformed data.
The user interface is kept simple - an unsolved (incomplete) Sudoku puzzle is fed in from an ASCII text file with a prescribed format, and the program outputs the solved (completed) Sudoku puzzle to the standard output stream. Currently the program expects the text file name to be specified in the code and displays appropriate error messages should the file contents not be formatted according to the expected format. This program also offers a function to convert puzzles stored on a single line to the format used in the program.
The following features are the focus of this implementation:
- Object-oriented organization
- STL data containers
- Guarded inputs/outputs
- Automated unit testing
- Multi-threaded support
- No-throw exception safety
The usage of templates and inheritance/polymorphism are not required in this design model.
The following command line arguments are provided, where the name of the program is "main":
- ./main test => Perform unit testing.
- ./main safe => Check that the puzzles are valid.
- ./main fast => Solve the puzzles in multi-threaded mode.
If no arguments are specified, the program will assume the given puzzles are valid and solve them in single-threaded mode. The program is also able to print out solver statistics but this option is turned off by default.
The system-level unit-testing for this project uses CppUnitLite and places a greater focus on the inputs/outputs and checking that the program can handle various cases of improperly-formatted data, and not so much on the underlying algorithm, which is assumed to be correct. The minimal testing is applied to the algorithm itself to verify that it works and that there are no memory leaks or out-of-bound array elements; other edge cases are not tested.
Single-threaded execution for 10,000 & 20,000 puzzles:
MODE | 10,000 Take 1 | 10,000 Take 2 | 20,000 Take 1 | 20,000 Take 2 |
---|---|---|---|---|
REAL | 0m 3.295s | 0m 3.348s | 0m 6.591s | 0m 6.661s |
USER | 0m 2.083s | 0m 2.169s | 0m 4.166s | 0m 4.232s |
SYS | 0m 0.633s | 0m 0.590s | 0m 1.252s | 0m 1.245s |
The call graph above, profiled using Valgrind, shows that the program spends about 40% of the time solving the puzzles and 60% of the time printing to the output stream. Since the solver algorithm itself is assumed to be efficient, the time spent on peripheral tasks like I/O should ideally be minimized, and one possible future solution might be to push all the output onto a string buffer before streaming the complete output only at the end.
Multi-threaded execution for 10,000 & 20,000 puzzles:
MODE | 10,000 Take 1 | 10,000 Take 2 | 20,000 Take 1 | 20,000 Take 2 |
---|---|---|---|---|
REAL | 0m 2.349s | 0m 2.429s | 0m 4.601s | 0m 4.656s |
USER | 0m 2.836s | 0m 2.854s | 0m 5.624s | 0m 5.633s |
SYS | 0m 0.622s | 0m 0.646s | 0m 1.229s | 0m 1.232s |
On the other hand, the 2 call graphs above show the program execution over the main thread and 1 of the 4 child threads. Over here, though, the situation is somewhat reversed in that the threads spend about 60% of the time solving the puzzles and 40% of the time on output. This might seem like an improvement from single-threaded execution but the absolute times did not really show a proportional improvement; from this exercise it is suggested that the work required to fork & join the threads isn't negligible compared to the work done in each thread, at least for 20,000 puzzles.
The algorithm layout is suitable to be organized into 2 groups - 1 group to handle the file I/O, and 1 group to handle the actual puzzle-solving. Where necessary, STL containers (e.g. std::vector, std::stringstream) are used in place of the existing C-style arrays, and pre-existing global data members from the original program are encapsulated in the class definition. Operators are overloaded only as needed where helpful in handling I/O streaming operations.
In terms of Exception safety, the goal is to have no exceptions thrown during the execution of the critical parts of the program (optional parts like file format conversion or unit testing are not scrutinized for exception safety). In these parts the usage of STL components that don't throw or are explicitly marked 'noexcept' is maximized, and usage of STL components that do throw is carefully guarded if the possible exceptions that can arise come from program bugs rather than unforseen circumstances; e.g. std::string::erase() throws std::out_of_range if index > size().
All other sources of possible exceptions in this program stem from either having not enough memory (e.g. std::vector::reserve(), std::make_unique, std::string) or some other serious system error (e.g. std::thread, std::mutex), and offering exception safety in these regards involve testing the system in addition to testing the program. Each Sudoku object requires at most 900*sizeof(int) bytes of memory, and also if the std::string allocations for each object don't exceed 400 bytes, an x86-64 Linux system with 4 GB free RAM that's not running any other userspace processes should be able to handle up to 1,000,000 puzzles without throwing any exceptions within the program's lifetime.
Original Version Source [MIT Licence] : http://bit.ly/1zAXbua
CNN Article: http://edition.cnn.com/2015/05/06/asia/singapore-pm-code-sudoku/
Sample Puzzles From: https://projecteuler.net/problem=96 and http://www.printable-sudoku-puzzles.com/wfiles/