1 /* 2 Copyright (c) 2021 Intel Corporation 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 #define TBB_PREVIEW_WAITING_FOR_WORKERS 1 18 #define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 19 20 #include "common/config.h" 21 22 #include <oneapi/tbb/task_arena.h> 23 #include <oneapi/tbb/concurrent_vector.h> 24 #include <oneapi/tbb/rw_mutex.h> 25 #include <oneapi/tbb/task_group.h> 26 #include <oneapi/tbb/parallel_for.h> 27 28 #include <oneapi/tbb/global_control.h> 29 30 #include "common/test.h" 31 #include "common/utils.h" 32 #include "common/utils_concurrency_limit.h" 33 #include "common/spin_barrier.h" 34 35 #include <stdlib.h> // C11/POSIX aligned_alloc 36 #include <random> 37 38 39 //! \file test_scheduler_mix.cpp 40 //! \brief Test for [scheduler.task_arena scheduler.task_scheduler_observer] specification 41 42 const std::uint64_t maxNumActions = 1 * 100 * 1000; 43 static std::atomic<std::uint64_t> globalNumActions{}; 44 45 //using Random = utils::FastRandom<>; 46 class Random { 47 struct State { 48 std::random_device rd; 49 std::mt19937 gen; 50 std::uniform_int_distribution<> dist; 51 52 State() : gen(rd()), dist(0, std::numeric_limits<unsigned short>::max()) {} 53 54 int get() { 55 return dist(gen); 56 } 57 }; 58 static thread_local State* mState; 59 tbb::concurrent_vector<State*> mStateList; 60 public: 61 ~Random() { 62 for (auto s : mStateList) { 63 delete s; 64 } 65 } 66 67 int get() { 68 auto& s = mState; 69 if (!s) { 70 s = new State; 71 mStateList.push_back(s); 72 } 73 return s->get(); 74 } 75 }; 76 77 thread_local Random::State* Random::mState = nullptr; 78 79 80 void* aligned_malloc(std::size_t alignment, std::size_t size) { 81 #if _WIN32 82 return _aligned_malloc(size, alignment); 83 #elif __unix__ || __APPLE__ 84 void* ptr{}; 85 int res = posix_memalign(&ptr, alignment, size); 86 CHECK(res == 0); 87 return ptr; 88 #else 89 return aligned_alloc(alignment, size); 90 #endif 91 } 92 93 void aligned_free(void* ptr) { 94 #if _WIN32 95 _aligned_free(ptr); 96 #else 97 free(ptr); 98 #endif 99 } 100 101 template <typename T, std::size_t Alignment> 102 class PtrRWMutex { 103 static const std::size_t maxThreads = (Alignment >> 1) - 1; 104 static const std::uintptr_t READER_MASK = maxThreads; // 7F.. 105 static const std::uintptr_t LOCKED = Alignment - 1; // FF.. 106 static const std::uintptr_t LOCKED_MASK = LOCKED; // FF.. 107 static const std::uintptr_t LOCK_PENDING = READER_MASK + 1; // 80.. 108 109 std::atomic<std::uintptr_t> mState; 110 111 T* pointer() { 112 return reinterpret_cast<T*>(state() & ~LOCKED_MASK); 113 } 114 115 std::uintptr_t state() { 116 return mState.load(std::memory_order_relaxed); 117 } 118 119 public: 120 class ScopedLock { 121 public: 122 constexpr ScopedLock() : mMutex(nullptr), mIsWriter(false) {} 123 //! Acquire lock on given mutex. 124 ScopedLock(PtrRWMutex& m, bool write = true) : mMutex(nullptr) { 125 CHECK_FAST(write == true); 126 acquire(m); 127 } 128 //! Release lock (if lock is held). 129 ~ScopedLock() { 130 if (mMutex) { 131 release(); 132 } 133 } 134 //! No Copy 135 ScopedLock(const ScopedLock&) = delete; 136 ScopedLock& operator=(const ScopedLock&) = delete; 137 138 //! Acquire lock on given mutex. 139 void acquire(PtrRWMutex& m) { 140 CHECK_FAST(mMutex == nullptr); 141 mIsWriter = true; 142 mMutex = &m; 143 mMutex->lock(); 144 } 145 146 //! Try acquire lock on given mutex. 147 bool tryAcquire(PtrRWMutex& m, bool write = true) { 148 bool succeed = write ? m.tryLock() : m.tryLockShared(); 149 if (succeed) { 150 mMutex = &m; 151 mIsWriter = write; 152 } 153 return succeed; 154 } 155 156 void clear() { 157 CHECK_FAST(mMutex != nullptr); 158 CHECK_FAST(mIsWriter); 159 PtrRWMutex* m = mMutex; 160 mMutex = nullptr; 161 m->clear(); 162 } 163 164 //! Release lock. 165 void release() { 166 CHECK_FAST(mMutex != nullptr); 167 PtrRWMutex* m = mMutex; 168 mMutex = nullptr; 169 170 if (mIsWriter) { 171 m->unlock(); 172 } 173 else { 174 m->unlockShared(); 175 } 176 } 177 protected: 178 PtrRWMutex* mMutex{}; 179 bool mIsWriter{}; 180 }; 181 182 bool trySet(T* ptr) { 183 auto p = reinterpret_cast<std::uintptr_t>(ptr); 184 CHECK_FAST((p & (Alignment - 1)) == 0); 185 if (!state()) { 186 std::uintptr_t expected = 0; 187 if (mState.compare_exchange_strong(expected, p)) { 188 return true; 189 } 190 } 191 return false; 192 } 193 194 void clear() { 195 CHECK_FAST((state() & LOCKED_MASK) == LOCKED); 196 mState.store(0, std::memory_order_relaxed); 197 } 198 199 bool tryLock() { 200 auto v = state(); 201 if (v == 0) { 202 return false; 203 } 204 CHECK_FAST((v & LOCKED_MASK) == LOCKED || (v & READER_MASK) < maxThreads); 205 if ((v & READER_MASK) == 0) { 206 if (mState.compare_exchange_strong(v, v | LOCKED)) { 207 return true; 208 } 209 } 210 return false; 211 } 212 213 bool tryLockShared() { 214 auto v = state(); 215 if (v == 0) { 216 return false; 217 } 218 CHECK_FAST((v & LOCKED_MASK) == LOCKED || (v & READER_MASK) < maxThreads); 219 if ((v & LOCKED_MASK) != LOCKED && (v & LOCK_PENDING) == 0) { 220 if (mState.compare_exchange_strong(v, v + 1)) { 221 return true; 222 } 223 } 224 return false; 225 } 226 227 void lock() { 228 auto v = state(); 229 mState.compare_exchange_strong(v, v | LOCK_PENDING); 230 while (!tryLock()) { 231 utils::yield(); 232 } 233 } 234 235 void unlock() { 236 auto v = state(); 237 CHECK_FAST((v & LOCKED_MASK) == LOCKED); 238 mState.store(v & ~LOCKED, std::memory_order_release); 239 } 240 241 void unlockShared() { 242 auto v = state(); 243 CHECK_FAST((v & LOCKED_MASK) != LOCKED); 244 CHECK_FAST((v & READER_MASK) > 0); 245 mState -= 1; 246 } 247 248 operator bool() const { 249 return pointer() != 0; 250 } 251 252 T* get() { 253 return pointer(); 254 } 255 }; 256 257 class Statistics { 258 public: 259 enum ACTION { 260 ArenaCreate, 261 ArenaDestroy, 262 ArenaAcquire, 263 skippedArenaCreate, 264 skippedArenaDestroy, 265 skippedArenaAcquire, 266 ParallelAlgorithm, 267 ArenaEnqueue, 268 ArenaExecute, 269 numActions 270 }; 271 272 static const char* const mStatNames[numActions]; 273 private: 274 struct StatType { 275 StatType() : mCounters() {} 276 std::array<std::uint64_t, numActions> mCounters; 277 }; 278 279 tbb::concurrent_vector<StatType*> mStatsList; 280 static thread_local StatType* mStats; 281 282 StatType& get() { 283 auto& s = mStats; 284 if (!s) { 285 s = new StatType; 286 mStatsList.push_back(s); 287 } 288 return *s; 289 } 290 public: 291 ~Statistics() { 292 for (auto s : mStatsList) { 293 delete s; 294 } 295 } 296 297 void notify(ACTION a) { 298 ++get().mCounters[a]; 299 } 300 301 void report() { 302 StatType summary; 303 for (auto& s : mStatsList) { 304 for (int i = 0; i < numActions; ++i) { 305 summary.mCounters[i] += s->mCounters[i]; 306 } 307 } 308 std::cout << std::endl << "Statistics:" << std::endl; 309 std::cout << "Total actions: " << globalNumActions << std::endl; 310 for (int i = 0; i < numActions; ++i) { 311 std::cout << mStatNames[i] << ": " << summary.mCounters[i] << std::endl; 312 } 313 } 314 }; 315 316 317 const char* const Statistics::mStatNames[Statistics::numActions] = { 318 "Arena create", "Arena destroy", "Arena acquire", 319 "Skipped arena create", "Skipped arena destroy", "Skipped arena acquire", 320 "Parallel algorithm", "Arena enqueue", "Arena execute" 321 }; 322 thread_local Statistics::StatType* Statistics::mStats; 323 324 static Statistics gStats; 325 326 class LifetimeTracker { 327 public: 328 LifetimeTracker() = default; 329 330 class Guard { 331 public: 332 Guard(LifetimeTracker* obj) { 333 if (!(obj->mReferences.load(std::memory_order_relaxed) & SHUTDOWN_FLAG)) { 334 if (obj->mReferences.fetch_add(REFERENCE_FLAG) & SHUTDOWN_FLAG) { 335 obj->mReferences.fetch_sub(REFERENCE_FLAG); 336 } else { 337 mObj = obj; 338 } 339 } 340 } 341 342 Guard(Guard&& ing) : mObj(ing.mObj) { 343 ing.mObj = nullptr; 344 } 345 346 ~Guard() { 347 if (mObj != nullptr) { 348 mObj->mReferences.fetch_sub(REFERENCE_FLAG); 349 } 350 } 351 352 bool continue_execution() { 353 return mObj != nullptr; 354 } 355 356 private: 357 LifetimeTracker* mObj{nullptr}; 358 }; 359 360 Guard makeGuard() { 361 return Guard(this); 362 } 363 364 void signalShutdown() { 365 mReferences.fetch_add(SHUTDOWN_FLAG); 366 } 367 368 void waitCompletion() { 369 utils::SpinWaitUntilEq(mReferences, SHUTDOWN_FLAG); 370 } 371 372 private: 373 friend class Guard; 374 static constexpr std::uintptr_t SHUTDOWN_FLAG = 1; 375 static constexpr std::uintptr_t REFERENCE_FLAG = 1 << 1; 376 std::atomic<std::uintptr_t> mReferences{}; 377 }; 378 379 class ArenaTable { 380 static const std::size_t maxArenas = 64; 381 static const std::size_t maxThreads = 1 << 9; 382 static const std::size_t arenaAligment = maxThreads << 1; 383 384 using ArenaPtrRWMutex = PtrRWMutex<tbb::task_arena, arenaAligment>; 385 std::array<ArenaPtrRWMutex, maxArenas> mArenaTable; 386 387 struct ThreadState { 388 bool lockedArenas[maxArenas]{}; 389 int arenaIdxStack[maxArenas]; 390 int level{}; 391 }; 392 393 LifetimeTracker mLifetimeTracker{}; 394 static thread_local ThreadState mThreadState; 395 396 template <typename F> 397 auto find_arena(std::size_t start, F f) -> decltype(f(std::declval<ArenaPtrRWMutex&>(), std::size_t{})) { 398 for (std::size_t idx = start, i = 0; i < maxArenas; ++i, idx = (idx + 1) % maxArenas) { 399 auto res = f(mArenaTable[idx], idx); 400 if (res) { 401 return res; 402 } 403 } 404 return {}; 405 } 406 407 public: 408 using ScopedLock = ArenaPtrRWMutex::ScopedLock; 409 410 void create(Random& rnd) { 411 auto guard = mLifetimeTracker.makeGuard(); 412 if (guard.continue_execution()) { 413 int num_threads = rnd.get() % utils::get_platform_max_threads() + 1; 414 unsigned int num_reserved = rnd.get() % num_threads; 415 tbb::task_arena::priority priorities[] = { tbb::task_arena::priority::low , tbb::task_arena::priority::normal, tbb::task_arena::priority::high }; 416 tbb::task_arena::priority priority = priorities[rnd.get() % 3]; 417 418 tbb::task_arena* a = new (aligned_malloc(arenaAligment, arenaAligment)) tbb::task_arena{ num_threads , num_reserved , priority }; 419 420 if (!find_arena(rnd.get() % maxArenas, [a](ArenaPtrRWMutex& arena, std::size_t) -> bool { 421 if (arena.trySet(a)) { 422 return true; 423 } 424 return false; 425 })) 426 { 427 gStats.notify(Statistics::skippedArenaCreate); 428 a->~task_arena(); 429 aligned_free(a); 430 } 431 } 432 } 433 434 void destroy(Random& rnd) { 435 auto guard = mLifetimeTracker.makeGuard(); 436 if (guard.continue_execution()) { 437 auto& ts = mThreadState; 438 if (!find_arena(rnd.get() % maxArenas, [&ts](ArenaPtrRWMutex& arena, std::size_t idx) { 439 if (!ts.lockedArenas[idx]) { 440 ScopedLock lock; 441 if (lock.tryAcquire(arena, true)) { 442 auto a = arena.get(); 443 lock.clear(); 444 a->~task_arena(); 445 aligned_free(a); 446 return true; 447 } 448 } 449 return false; 450 })) 451 { 452 gStats.notify(Statistics::skippedArenaDestroy); 453 } 454 } 455 } 456 457 void shutdown() { 458 mLifetimeTracker.signalShutdown(); 459 mLifetimeTracker.waitCompletion(); 460 find_arena(0, [](ArenaPtrRWMutex& arena, std::size_t) { 461 if (arena.get()) { 462 ScopedLock lock{ arena, true }; 463 auto a = arena.get(); 464 lock.clear(); 465 a->~task_arena(); 466 aligned_free(a); 467 } 468 return false; 469 }); 470 } 471 472 std::pair<tbb::task_arena*, std::size_t> acquire(Random& rnd, ScopedLock& lock) { 473 auto guard = mLifetimeTracker.makeGuard(); 474 475 tbb::task_arena* a{nullptr}; 476 std::size_t resIdx{}; 477 if (guard.continue_execution()) { 478 auto& ts = mThreadState; 479 a = find_arena(rnd.get() % maxArenas, 480 [&ts, &lock, &resIdx](ArenaPtrRWMutex& arena, std::size_t idx) -> tbb::task_arena* { 481 if (!ts.lockedArenas[idx]) { 482 if (lock.tryAcquire(arena, false)) { 483 ts.lockedArenas[idx] = true; 484 ts.arenaIdxStack[ts.level++] = int(idx); 485 resIdx = idx; 486 return arena.get(); 487 } 488 } 489 return nullptr; 490 }); 491 if (!a) { 492 gStats.notify(Statistics::skippedArenaAcquire); 493 } 494 } 495 return { a, resIdx }; 496 } 497 498 void release(ScopedLock& lock) { 499 auto& ts = mThreadState; 500 CHECK_FAST(ts.level > 0); 501 auto idx = ts.arenaIdxStack[--ts.level]; 502 CHECK_FAST(ts.lockedArenas[idx]); 503 ts.lockedArenas[idx] = false; 504 lock.release(); 505 } 506 }; 507 508 thread_local ArenaTable::ThreadState ArenaTable::mThreadState; 509 510 static ArenaTable arenaTable; 511 static Random threadRandom; 512 513 enum ACTIONS { 514 arena_create, 515 arena_destroy, 516 arena_action, 517 parallel_algorithm, 518 // TODO: 519 // observer_attach, 520 // observer_detach, 521 // flow_graph, 522 // task_group, 523 // resumable_tasks, 524 525 num_actions 526 }; 527 528 void global_actor(); 529 530 template <ACTIONS action> 531 struct actor; 532 533 template <> 534 struct actor<arena_create> { 535 static void do_it(Random& r) { 536 arenaTable.create(r); 537 } 538 }; 539 540 template <> 541 struct actor<arena_destroy> { 542 static void do_it(Random& r) { 543 arenaTable.destroy(r); 544 } 545 }; 546 547 template <> 548 struct actor<arena_action> { 549 static void do_it(Random& r) { 550 static thread_local std::size_t arenaLevel = 0; 551 ArenaTable::ScopedLock lock; 552 auto entry = arenaTable.acquire(r, lock); 553 if (entry.first) { 554 enum arena_actions { 555 arena_execute, 556 arena_enqueue, 557 num_arena_actions 558 }; 559 auto process = r.get() % 2; 560 auto body = [process] { 561 if (process) { 562 tbb::detail::d1::wait_context wctx{ 1 }; 563 tbb::task_group_context ctx; 564 tbb::this_task_arena::enqueue([&wctx] { wctx.release(); }); 565 tbb::detail::d1::wait(wctx, ctx); 566 } else { 567 global_actor(); 568 } 569 }; 570 switch (r.get() % (16*num_arena_actions)) { 571 case arena_execute: 572 if (entry.second > arenaLevel) { 573 gStats.notify(Statistics::ArenaExecute); 574 auto oldArenaLevel = arenaLevel; 575 arenaLevel = entry.second; 576 entry.first->execute(body); 577 arenaLevel = oldArenaLevel; 578 break; 579 } 580 utils_fallthrough; 581 case arena_enqueue: 582 utils_fallthrough; 583 default: 584 gStats.notify(Statistics::ArenaEnqueue); 585 entry.first->enqueue([] { global_actor(); }); 586 break; 587 } 588 arenaTable.release(lock); 589 } 590 } 591 }; 592 593 template <> 594 struct actor<parallel_algorithm> { 595 static void do_it(Random& rnd) { 596 enum PARTITIONERS { 597 simpl_part, 598 auto_part, 599 aff_part, 600 static_part, 601 num_parts 602 }; 603 int sz = rnd.get() % 10000; 604 auto doGlbAction = rnd.get() % 1000 == 42; 605 auto body = [doGlbAction, sz](int i) { 606 if (i == sz / 2 && doGlbAction) { 607 global_actor(); 608 } 609 }; 610 611 switch (rnd.get() % num_parts) { 612 case simpl_part: 613 tbb::parallel_for(0, sz, body, tbb::simple_partitioner{}); break; 614 case auto_part: 615 tbb::parallel_for(0, sz, body, tbb::auto_partitioner{}); break; 616 case aff_part: 617 { 618 tbb::affinity_partitioner aff; 619 tbb::parallel_for(0, sz, body, aff); break; 620 } 621 case static_part: 622 tbb::parallel_for(0, sz, body, tbb::static_partitioner{}); break; 623 } 624 } 625 }; 626 627 void global_actor() { 628 static thread_local std::uint64_t localNumActions{}; 629 630 while (globalNumActions < maxNumActions) { 631 auto& rnd = threadRandom; 632 switch (rnd.get() % num_actions) { 633 case arena_create: gStats.notify(Statistics::ArenaCreate); actor<arena_create>::do_it(rnd); break; 634 case arena_destroy: gStats.notify(Statistics::ArenaDestroy); actor<arena_destroy>::do_it(rnd); break; 635 case arena_action: gStats.notify(Statistics::ArenaAcquire); actor<arena_action>::do_it(rnd); break; 636 case parallel_algorithm: gStats.notify(Statistics::ParallelAlgorithm); actor<parallel_algorithm>::do_it(rnd); break; 637 } 638 639 if (++localNumActions == 100) { 640 localNumActions = 0; 641 globalNumActions += 100; 642 643 // TODO: Enable statistics 644 // static std::mutex mutex; 645 // std::lock_guard<std::mutex> lock{ mutex }; 646 // std::cout << globalNumActions << "\r" << std::flush; 647 } 648 } 649 } 650 651 #if TBB_USE_EXCEPTIONS 652 //! \brief \ref stress 653 TEST_CASE("Stress test with mixing functionality") { 654 // TODO add thread recreation 655 // TODO: Enable statistics 656 // tbb::task_scheduler_handle handle = tbb::task_scheduler_handle::get(); 657 658 const std::size_t numExtraThreads = 16; 659 utils::SpinBarrier startBarrier{numExtraThreads}; 660 utils::NativeParallelFor(numExtraThreads, [&startBarrier](std::size_t) { 661 startBarrier.wait(); 662 global_actor(); 663 }); 664 665 arenaTable.shutdown(); 666 667 // tbb::finalize(handle, std::nothrow_t{}); 668 669 // gStats.report(); 670 } 671 #endif 672