Skip to content

Commit 5eee176

Browse files
Fix thread_pool_base bulk with narrow integer shapes (#2317)
* Fix bulk worker counts for narrow integer shapes * Test signed 32-bit bulk shapes with very large pool sizes * Cover maximal uint8 bulk shape and explicitly include utility
1 parent d387428 commit 5eee176

2 files changed

Lines changed: 135 additions & 6 deletions

File tree

‎include/exec/thread_pool_base.hpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ import stdexec;
3838
# include "../stdexec/__detail/__transform_completion_signatures.hpp"
3939
# include "../stdexec/__detail/__type_traits.hpp"
4040

41+
# include <algorithm>
4142
# include <atomic>
4243
# include <concepts>
4344
# include <cstdint>
4445
# include <exception>
4546
# include <tuple>
47+
# include <type_traits>
4648
# include <utility>
4749
# include <variant>
4850
#endif
@@ -230,14 +232,15 @@ namespace experimental::execution
230232
[[nodiscard]]
231233
auto num_agents_required() const -> std::uint32_t
232234
{
235+
using common_shape_t = std::common_type_t<Shape, std::uint32_t>;
233236
auto const parallelism = parallelize_ ? pool_.available_parallelism()
234237
: static_cast<std::uint32_t>(1);
235238

236239
// With work stealing, is std::min necessary, or can we feel free to
237240
// ask for more agents (tasks) than we can actually deal with at one
238241
// time?
239242
return Shape{} < shape_
240-
? static_cast<std::uint32_t>((std::min) (shape_, static_cast<Shape>(parallelism)))
243+
? static_cast<std::uint32_t>((std::min<common_shape_t>) (shape_, parallelism))
241244
: 0;
242245
}
243246

‎test/exec/test_thread_pool_base.cpp‎

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
#include <stdexec/execution.hpp>
1919
#include <test_common/catch2.hpp>
2020

21+
#include <array>
2122
#include <cstdint>
2223
#include <exception>
24+
#include <limits>
25+
#include <utility>
2326

2427
namespace ex = STDEXEC;
2528

@@ -32,7 +35,7 @@ namespace
3235
[[nodiscard]]
3336
auto available_parallelism() const noexcept -> std::uint32_t
3437
{
35-
return 1;
38+
return parallelism_;
3639
}
3740

3841
[[nodiscard]]
@@ -47,7 +50,8 @@ namespace
4750
task->execute_(task, tid);
4851
}
4952

50-
std::uint32_t enqueued_ = 0;
53+
std::uint32_t parallelism_ = 1;
54+
std::uint32_t enqueued_ = 0;
5155
};
5256

5357
struct value_capture_error
@@ -126,15 +130,137 @@ namespace
126130
CHECK(pool.enqueued_ == 1);
127131
}
128132

129-
TEST_CASE("thread_pool_base bulk does not invoke the function with a negative shape",
133+
TEMPLATE_TEST_CASE("thread_pool_base bulk handles pool sizes wider than the shape",
134+
"[thread_pool_base][bulk]",
135+
std::uint8_t,
136+
std::int8_t,
137+
std::uint16_t,
138+
std::int16_t,
139+
std::int32_t)
140+
{
141+
inline_test_thread_pool pool;
142+
pool.parallelism_ = GENERATE(256u, 257u, 65'536u, 65'537u, 0x80000000u, 0xffffffffu);
143+
completion_state state;
144+
std::array<int, 5> visits{};
145+
auto const shape = GENERATE(TestType{1}, TestType{3}, TestType{5});
146+
147+
SECTION("chunked")
148+
{
149+
auto sndr = ex::schedule(pool.get_scheduler())
150+
| ex::bulk_chunked(ex::par,
151+
shape,
152+
[&](TestType begin, TestType end) noexcept
153+
{
154+
for (auto i = begin; i < end; ++i)
155+
{
156+
++visits[i];
157+
}
158+
});
159+
auto op = ex::connect(std::move(sndr), counting_receiver{&state});
160+
ex::start(op);
161+
}
162+
163+
SECTION("unchunked")
164+
{
165+
auto sndr = ex::schedule(pool.get_scheduler())
166+
| ex::bulk_unchunked(ex::par, shape, [&](TestType i) noexcept { ++visits[i]; });
167+
auto op = ex::connect(std::move(sndr), counting_receiver{&state});
168+
ex::start(op);
169+
}
170+
171+
REQUIRE(state.completions_ == 1);
172+
CHECK_FALSE(state.error_);
173+
CHECK(pool.enqueued_ == static_cast<std::uint32_t>(shape) + 1);
174+
for (std::size_t i = 0; i < visits.size(); ++i)
175+
{
176+
CHECK(visits[i] == (i < static_cast<std::size_t>(shape) ? 1 : 0));
177+
}
178+
}
179+
180+
TEST_CASE("thread_pool_base bulk covers a maximal uint8 shape", "[thread_pool_base][bulk]")
181+
{
182+
inline_test_thread_pool pool;
183+
pool.parallelism_ = 256;
184+
completion_state state;
185+
std::array<int, 255> visits{};
186+
187+
auto sndr = ex::schedule(pool.get_scheduler())
188+
| ex::bulk_unchunked(ex::par,
189+
std::uint8_t{255},
190+
[&](std::uint8_t i) noexcept { ++visits[i]; });
191+
auto op = ex::connect(std::move(sndr), counting_receiver{&state});
192+
ex::start(op);
193+
194+
CHECK(state.completions_ == 1);
195+
CHECK_FALSE(state.error_);
196+
CHECK(pool.enqueued_ == 256);
197+
for (int count: visits)
198+
{
199+
CHECK(count == 1);
200+
}
201+
}
202+
203+
TEST_CASE("thread_pool_base bulk preserves shapes wider than the worker count",
204+
"[thread_pool_base][bulk]")
205+
{
206+
inline_test_thread_pool pool;
207+
pool.parallelism_ = 2;
208+
completion_state state;
209+
auto const shape = GENERATE(std::uint64_t{1} << 32,
210+
(std::numeric_limits<std::uint64_t>::max)());
211+
std::uint64_t visited = 0;
212+
213+
auto sndr = ex::schedule(pool.get_scheduler())
214+
| ex::bulk_chunked(ex::par,
215+
shape,
216+
[&](std::uint64_t begin, std::uint64_t end) noexcept
217+
{ visited += end - begin; });
218+
auto op = ex::connect(std::move(sndr), counting_receiver{&state});
219+
ex::start(op);
220+
221+
CHECK(state.completions_ == 1);
222+
CHECK_FALSE(state.error_);
223+
CHECK(visited == shape);
224+
CHECK(pool.enqueued_ == 3);
225+
}
226+
227+
TEST_CASE("thread_pool_base sequential bulk uses one worker with a narrow shape",
228+
"[thread_pool_base][bulk]")
229+
{
230+
inline_test_thread_pool pool;
231+
pool.parallelism_ = 256;
232+
completion_state state;
233+
int visited = 0;
234+
int bulk_calls = 0;
235+
236+
auto sndr = ex::schedule(pool.get_scheduler())
237+
| ex::bulk_chunked(ex::seq,
238+
std::uint8_t{3},
239+
[&](std::uint8_t begin, std::uint8_t end) noexcept
240+
{
241+
visited += end - begin;
242+
++bulk_calls;
243+
});
244+
auto op = ex::connect(std::move(sndr), counting_receiver{&state});
245+
ex::start(op);
246+
247+
CHECK(state.completions_ == 1);
248+
CHECK_FALSE(state.error_);
249+
CHECK(visited == 3);
250+
CHECK(bulk_calls == 1);
251+
CHECK(pool.enqueued_ == 2);
252+
}
253+
254+
TEST_CASE("thread_pool_base bulk does not invoke the function with a nonpositive shape",
130255
"[thread_pool_base][bulk]")
131256
{
132257
inline_test_thread_pool pool;
133258
completion_state state;
134259
int bulk_calls = 0;
135260

136-
auto sndr = ex::schedule(pool.get_scheduler())
137-
| ex::bulk_chunked(ex::par, -1, [&](int, int) noexcept { ++bulk_calls; });
261+
auto const shape = GENERATE(-1, 0);
262+
auto sndr = ex::schedule(pool.get_scheduler())
263+
| ex::bulk_chunked(ex::par, shape, [&](int, int) noexcept { ++bulk_calls; });
138264
auto op = ex::connect(std::move(sndr), counting_receiver{&state});
139265

140266
ex::start(op);

0 commit comments

Comments
 (0)