-
Notifications
You must be signed in to change notification settings - Fork 118
Improve parallelism by solving most difficult deals first #216
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
base: develop
Are you sure you want to change the base?
Changes from all commits
2182b0f
8ecebc8
6234595
b08925f
2d57e8d
f4ed912
f733867
fa2c84c
07a33cd
5e34528
6ead0ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,12 +8,15 @@ | |
| */ | ||
|
|
||
| #include "calc_tables.hpp" | ||
| #include <algorithm> | ||
| #include <numeric> | ||
| #include <vector> | ||
|
|
||
| #include <pbn.hpp> | ||
| #include <solve_board.hpp> | ||
| #include <api/solve_board.hpp> | ||
| #include <solver_if.hpp> | ||
| #include <lookup_tables/lookup_tables.hpp> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should probably pull and compile, but I don't see what's being used from this header. fanout comes from scheduler.hpp and all the other called functions appear to be in std:: namespace. What did I miss? |
||
| #include <system/memory.hpp> | ||
| #include <system/parallel_boards.hpp> | ||
| #include <system/scheduler.hpp> | ||
|
|
@@ -23,11 +26,11 @@ | |
| extern Memory memory; | ||
| extern Scheduler scheduler; | ||
|
|
||
|
tameware marked this conversation as resolved.
|
||
| // Legacy overload (creates temporary context) | ||
| auto calc_all_boards_n( | ||
| Boards * bop, | ||
| SolvedBoards * solvedp, | ||
| int max_threads = 0) -> int; | ||
| int max_threads = 0, | ||
| bool difficulty_sort = true) -> int; | ||
|
|
||
|
|
||
| auto calc_single_common_internal( | ||
|
|
@@ -110,7 +113,8 @@ auto calc_all_boards_n( | |
| auto calc_all_boards_n( | ||
| Boards * bop, | ||
| SolvedBoards * solvedp, | ||
| int max_threads) -> int | ||
| int max_threads, | ||
| bool difficulty_sort) -> int | ||
| { | ||
| const int n = bop->no_of_boards; | ||
| if (n > MAXNOOFBOARDS) | ||
|
|
@@ -137,11 +141,30 @@ auto calc_all_boards_n( | |
| else | ||
| { | ||
| std::vector<SolverContext> contexts(static_cast<unsigned>(nthreads)); | ||
|
|
||
| // Dispatch hardest boards first to shorten the parallel tail. This only | ||
| // helps across distinct deals (batch calc); for a single deal every board | ||
| // shares one fanout, so the sort is skipped (it would be a no-op anyway). | ||
| std::vector<int> order; | ||
| if (difficulty_sort) | ||
| { | ||
| std::vector<int> fanout(static_cast<unsigned>(n)); | ||
| for (int i = 0; i < n; i++) | ||
| fanout[static_cast<unsigned>(i)] = deal_fanout(bop->deals[i]); | ||
| order.resize(static_cast<unsigned>(n)); | ||
| std::iota(order.begin(), order.end(), 0); | ||
| std::stable_sort(order.begin(), order.end(), | ||
| [&](const int a, const int b) { | ||
| return fanout[static_cast<unsigned>(a)] > fanout[static_cast<unsigned>(b)]; | ||
| }); | ||
| } | ||
|
|
||
| err = parallel_all_boards_n(n, nthreads, | ||
| [&](const int worker_id, const int bno) -> int { | ||
| return calc_single_common_internal( | ||
| contexts[static_cast<unsigned>(worker_id)], *bop, *solvedp, bno); | ||
| }); | ||
| }, | ||
| order.empty() ? nullptr : &order); | ||
| } | ||
|
|
||
| END_BLOCK_TIMER; | ||
|
|
@@ -192,7 +215,8 @@ int STDCALL CalcDDtableN( | |
| ind++; | ||
| } | ||
|
|
||
| int res = calc_all_boards_n(&bo, &solved, maxThreads); | ||
| // Single deal: all boards share one deal, so hardest-first sorting is a no-op. | ||
| int res = calc_all_boards_n(&bo, &solved, maxThreads, /*difficulty_sort=*/false); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. trivial Inline variable-name comment isn't necessary. |
||
| if (res != 1) | ||
| return res; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ | |
|
|
||
| #include <algorithm> | ||
| #include <atomic> | ||
| #include <iterator> | ||
| #include <numeric> | ||
| #include <thread> | ||
| #include <vector> | ||
|
|
||
|
|
@@ -31,23 +33,57 @@ auto resolve_worker_count( | |
| } | ||
|
|
||
|
|
||
| static auto is_permutation_of_range( | ||
| const std::vector<int>& order, | ||
| const int count) -> bool | ||
| { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have Standard library functions are at least expected to be faster and reduce the amount of code we need to write.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice find! Fixed.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or not… Copilot reports that the change is less efficient: is_permutation_of_range() currently re-sorts the order vector and builds an intermediate common vector via set_intersection. This is O(n log n) plus extra allocations and will run on every parallel_all_boards_n() invocation that supplies an order (e.g. batch calc), adding overhead in a performance-sensitive path. You can validate a permutation of [0, count) in O(n) without sorting by tracking seen indices and checking bounds/duplicates. Thoughts, @zzcgumn?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've had another look. I agree with Copilot - I prefer the original implementation:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. set_difference makes sense if you need to know what the difference is. The use case here doesn't need to prove as hard a problem. |
||
| std::vector<int> sorted(order); | ||
| std::sort(sorted.begin(), sorted.end()); | ||
|
|
||
| std::vector<int> expected(static_cast<unsigned>(count)); | ||
| std::iota(expected.begin(), expected.end(), 0); | ||
|
|
||
| std::vector<int> common; | ||
| common.reserve(static_cast<unsigned>(count)); | ||
| std::set_intersection( | ||
| sorted.begin(), sorted.end(), | ||
| expected.begin(), expected.end(), | ||
| std::back_inserter(common)); | ||
|
|
||
| return static_cast<int>(common.size()) == count; | ||
| } | ||
|
|
||
|
|
||
| auto parallel_all_boards_n( | ||
| const int count, | ||
| const int worker_cap, | ||
| const std::function<int(int worker_id, int bno)>& process_board) -> int | ||
| const std::function<int(int worker_id, int bno)>& process_board, | ||
| const std::vector<int>* order) -> int | ||
| { | ||
| if (count <= 0) | ||
| { | ||
| return RETURN_NO_FAULT; | ||
| } | ||
|
|
||
| // Map a dispatch slot to the board number to process. With an order, hand out | ||
| // boards in that sequence (e.g. hardest first); otherwise in index order. The | ||
| // order is only honored when it is a valid permutation of [0, count); a | ||
| // malformed order falls back to index order to avoid invalid board indices. | ||
| const bool use_order = | ||
| (order != nullptr && | ||
| static_cast<int>(order->size()) == count && | ||
| is_permutation_of_range(*order, count)); | ||
| auto board_of = [&](const int slot) -> int { | ||
| return use_order ? (*order)[static_cast<unsigned>(slot)] : slot; | ||
| }; | ||
|
|
||
| const int workers = resolve_worker_count(worker_cap, count); | ||
|
|
||
| if (workers == 1) | ||
| { | ||
| for (int bno = 0; bno < count; ++bno) | ||
| for (int slot = 0; slot < count; ++slot) | ||
| { | ||
| const int rc = process_board(0, bno); | ||
| const int rc = process_board(0, board_of(slot)); | ||
| if (rc != RETURN_NO_FAULT) | ||
| { | ||
| return rc; | ||
|
|
@@ -62,11 +98,12 @@ auto parallel_all_boards_n( | |
| auto worker = [&](const int worker_id) { | ||
| for (;;) | ||
| { | ||
| const int bno = next.fetch_add(1, std::memory_order_relaxed); | ||
| if (bno >= count || first_error.load(std::memory_order_relaxed) != RETURN_NO_FAULT) | ||
| const int slot = next.fetch_add(1, std::memory_order_relaxed); | ||
| if (slot >= count || first_error.load(std::memory_order_relaxed) != RETURN_NO_FAULT) | ||
| { | ||
| break; | ||
| } | ||
| const int bno = board_of(slot); | ||
|
|
||
| const int rc = process_board(worker_id, bno); | ||
| if (rc != RETURN_NO_FAULT) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -758,6 +758,12 @@ int Scheduler::Strength(const Deal& dl) const | |
|
|
||
|
|
||
| int Scheduler::Fanout(const Deal& dl) const | ||
| { | ||
| return deal_fanout(dl); | ||
| } | ||
|
|
||
|
|
||
| auto deal_fanout(const Deal& dl) -> int | ||
|
Comment on lines
+761
to
+766
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have any skin in this, but why not move Fanout from private to public instead? What are the advantages of exposing the function this way? (Ideological purity being a perfectly fine answer.) |
||
| { | ||
| // The fanout for a given suit and a given player is the number | ||
| // of bit groups, so KT982 has 3 groups. In a given suit the | ||
|
|
||
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.
Might as well fix the indenting problem at the same time.