Skip to content

Commit 1dd0923

Browse files
committed
refactor: Track BnB selection by index
This is a performance optimization - rather than track all visited values in a bool vector, track the selected index in a vector. This results in a complexity reduction of O(utxo_size) to O(selection_size).
1 parent b71a077 commit 1dd0923

File tree

1 file changed

+21
-25
lines changed

1 file changed

+21
-25
lines changed

src/wallet/coinselection.cpp

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
6666
{
6767
SelectionResult result(selection_target);
6868
CAmount curr_value = 0;
69-
70-
std::vector<bool> curr_selection; // select the utxo at this index
71-
curr_selection.reserve(utxo_pool.size());
69+
std::vector<size_t> curr_selection; // selected utxo indexes
7270

7371
// Calculate curr_available_value
7472
CAmount curr_available_value = 0;
@@ -85,11 +83,11 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
8583
std::sort(utxo_pool.begin(), utxo_pool.end(), descending);
8684

8785
CAmount curr_waste = 0;
88-
std::vector<bool> best_selection;
86+
std::vector<size_t> best_selection;
8987
CAmount best_waste = MAX_MONEY;
9088

9189
// Depth First search loop for choosing the UTXOs
92-
for (size_t i = 0; i < TOTAL_TRIES; ++i) {
90+
for (size_t i = 0, utxo_pool_index = 0; i < TOTAL_TRIES; ++i, ++utxo_pool_index) {
9391
// Conditions for starting a backtrack
9492
bool backtrack = false;
9593
if (curr_value + curr_available_value < selection_target || // Cannot possibly reach target with the amount remaining in the curr_available_value.
@@ -104,7 +102,6 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
104102
// explore any more UTXOs to avoid burning money like that.
105103
if (curr_waste <= best_waste) {
106104
best_selection = curr_selection;
107-
best_selection.resize(utxo_pool.size());
108105
best_waste = curr_waste;
109106
if (best_waste == 0) {
110107
break;
@@ -116,36 +113,37 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
116113

117114
// Backtracking, moving backwards
118115
if (backtrack) {
119-
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
120-
while (!curr_selection.empty() && !curr_selection.back()) {
121-
curr_selection.pop_back();
122-
curr_available_value += utxo_pool.at(curr_selection.size()).GetSelectionAmount();
123-
}
124-
125116
if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is untraversed. All solutions searched
126117
break;
127118
}
128119

120+
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
121+
for (--utxo_pool_index; utxo_pool_index > curr_selection.back(); --utxo_pool_index) {
122+
curr_available_value += utxo_pool.at(utxo_pool_index).GetSelectionAmount();
123+
}
124+
129125
// Output was included on previous iterations, try excluding now.
130-
curr_selection.back() = false;
131-
OutputGroup& utxo = utxo_pool.at(curr_selection.size() - 1);
126+
assert(utxo_pool_index == curr_selection.back());
127+
OutputGroup& utxo = utxo_pool.at(utxo_pool_index);
132128
curr_value -= utxo.GetSelectionAmount();
133129
curr_waste -= utxo.fee - utxo.long_term_fee;
130+
curr_selection.pop_back();
134131
} else { // Moving forwards, continuing down this branch
135-
OutputGroup& utxo = utxo_pool.at(curr_selection.size());
132+
OutputGroup& utxo = utxo_pool.at(utxo_pool_index);
136133

137134
// Remove this utxo from the curr_available_value utxo amount
138135
curr_available_value -= utxo.GetSelectionAmount();
139136

140137
// Avoid searching a branch if the previous UTXO has the same value and same waste and was excluded. Since the ratio of fee to
141138
// long term fee is the same, we only need to check if one of those values match in order to know that the waste is the same.
142-
if (!curr_selection.empty() && !curr_selection.back() &&
143-
utxo.GetSelectionAmount() == utxo_pool.at(curr_selection.size() - 1).GetSelectionAmount() &&
144-
utxo.fee == utxo_pool.at(curr_selection.size() - 1).fee) {
145-
curr_selection.push_back(false);
146-
} else {
139+
if (curr_selection.empty() ||
140+
// The previous index is included and therefore not relevant for exclusion shortcut
141+
(utxo_pool_index - 1) == curr_selection.back() ||
142+
utxo.GetSelectionAmount() != utxo_pool.at(utxo_pool_index - 1).GetSelectionAmount() ||
143+
utxo.fee != utxo_pool.at(utxo_pool_index - 1).fee)
144+
{
147145
// Inclusion branch first (Largest First Exploration)
148-
curr_selection.push_back(true);
146+
curr_selection.push_back(utxo_pool_index);
149147
curr_value += utxo.GetSelectionAmount();
150148
curr_waste += utxo.fee - utxo.long_term_fee;
151149
}
@@ -158,10 +156,8 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
158156
}
159157

160158
// Set output set
161-
for (size_t i = 0; i < best_selection.size(); ++i) {
162-
if (best_selection.at(i)) {
163-
result.AddInput(utxo_pool.at(i));
164-
}
159+
for (const size_t& i : best_selection) {
160+
result.AddInput(utxo_pool.at(i));
165161
}
166162

167163
return result;

0 commit comments

Comments
 (0)