-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Rework get_task and steal_task to better interact with out_of_work checks #1779
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: master
Are you sure you want to change the base?
Changes from 10 commits
13d8ce9
2a20605
c7122e8
e43b9a5
c1f635d
deda35d
c975c58
c068596
8782e6b
1eefab2
ddd859c
796db43
6f0d728
bedfb38
e35c2c2
baa3f2c
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 |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| /* | ||
| Copyright (c) 2005-2021 Intel Corporation | ||
| Copyright (c) 2005-2025 Intel Corporation | ||
| Copyright (c) 2025 UXL Foundation Contributors | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
|
|
@@ -25,21 +26,22 @@ namespace r1 { | |
| //------------------------------------------------------------------------ | ||
| // Arena Slot | ||
| //------------------------------------------------------------------------ | ||
| d1::task* arena_slot::get_task_impl(size_t T, execution_data_ext& ed, bool& tasks_omitted, isolation_type isolation) { | ||
| d1::task* arena_slot::get_task_impl(size_t T, execution_data_ext& ed, bool& tasks_skipped, isolation_type isolation) { | ||
| __TBB_ASSERT(tail.load(std::memory_order_relaxed) <= T || is_local_task_pool_quiescent(), | ||
| "Is it safe to get a task at position T?"); | ||
|
|
||
| d1::task* result = task_pool_ptr[T]; | ||
| __TBB_ASSERT(!is_poisoned( result ), "The poisoned task is going to be processed"); | ||
| __TBB_ASSERT(!is_poisoned( result ), "A poisoned task is going to be processed"); | ||
|
|
||
| if (!result) { | ||
| return nullptr; | ||
| } | ||
| bool omit = isolation != no_isolation && isolation != task_accessor::isolation(*result); | ||
| if (!omit && !task_accessor::is_proxy_task(*result)) { | ||
| bool skip = isolation != no_isolation && isolation != task_accessor::isolation(*result); | ||
| if (!skip && !task_accessor::is_proxy_task(*result)) { | ||
| return result; | ||
| } else if (omit) { | ||
| tasks_omitted = true; | ||
| } else if (skip) { | ||
| has_skipped_tasks.store(true, std::memory_order_relaxed); | ||
| tasks_skipped = true; | ||
| return nullptr; | ||
| } | ||
|
|
||
|
|
@@ -52,7 +54,7 @@ d1::task* arena_slot::get_task_impl(size_t T, execution_data_ext& ed, bool& task | |
| // Proxy was empty, so it's our responsibility to free it | ||
| tp.allocator.delete_object(&tp, ed); | ||
|
|
||
| if ( tasks_omitted ) { | ||
| if ( tasks_skipped ) { | ||
| task_pool_ptr[T] = nullptr; | ||
| } | ||
| return nullptr; | ||
|
|
@@ -65,8 +67,8 @@ d1::task* arena_slot::get_task(execution_data_ext& ed, isolation_type isolation) | |
| // The bounds of available tasks in the task pool. H0 is only used when the head bound is reached. | ||
| std::size_t H0 = (std::size_t)-1, T = T0; | ||
| d1::task* result = nullptr; | ||
| bool task_pool_empty = false; | ||
| bool tasks_omitted = false; | ||
| bool all_tasks_checked = false; | ||
| bool tasks_skipped = false; | ||
| do { | ||
| __TBB_ASSERT( !result, nullptr ); | ||
| // The full fence is required to sync the store of `tail` with the load of `head` (write-read barrier) | ||
|
|
@@ -81,67 +83,54 @@ d1::task* arena_slot::get_task(execution_data_ext& ed, isolation_type isolation) | |
| __TBB_ASSERT( H0 == head.load(std::memory_order_relaxed) | ||
| && T == tail.load(std::memory_order_relaxed) | ||
| && H0 == T + 1, "victim/thief arbitration algorithm failure" ); | ||
| reset_task_pool_and_leave(); | ||
| (tasks_skipped) ? release_task_pool() : reset_task_pool_and_leave(); | ||
| // No tasks in the task pool. | ||
| task_pool_empty = true; | ||
| all_tasks_checked = true; | ||
| break; | ||
| } else if ( H0 == T ) { | ||
| // There is only one task in the task pool. | ||
| reset_task_pool_and_leave(); | ||
| task_pool_empty = true; | ||
| // There is only one task in the task pool. If it can be taken, we want to reset the pool | ||
| if ( isolation != no_isolation && task_pool_ptr[T] && | ||
| isolation != task_accessor::isolation(*task_pool_ptr[T]) ) { | ||
| // The task will be skipped due to isolation mismatch | ||
| has_skipped_tasks.store(true, std::memory_order_relaxed); | ||
| tasks_skipped = true; | ||
| } | ||
| (tasks_skipped) ? release_task_pool() : reset_task_pool_and_leave(); | ||
| all_tasks_checked = true; | ||
| } else { | ||
| // Release task pool if there are still some tasks. | ||
| // After the release, the tail will be less than T, thus a thief | ||
| // will not attempt to get a task at position T. | ||
| release_task_pool(); | ||
| } | ||
| } | ||
| result = get_task_impl( T, ed, tasks_omitted, isolation ); | ||
| result = get_task_impl( T, ed, tasks_skipped, isolation ); | ||
kboyarinov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ( result ) { | ||
| poison_pointer( task_pool_ptr[T] ); | ||
| // If some tasks were skipped, we need to make a hole in position T. | ||
| if ( tasks_skipped ) { | ||
| // If all tasks have been checked, the taken task must be at the H0 position | ||
| __TBB_ASSERT( !all_tasks_checked || H0 == T, nullptr ); | ||
| task_pool_ptr[T] = nullptr; | ||
| } else { | ||
| poison_pointer( task_pool_ptr[T] ); | ||
| } | ||
| break; | ||
| } else if ( !tasks_omitted ) { | ||
| } else if ( !tasks_skipped ) { | ||
| poison_pointer( task_pool_ptr[T] ); | ||
| __TBB_ASSERT( T0 == T+1, nullptr ); | ||
| T0 = T; | ||
| } | ||
| } while ( !result && !task_pool_empty ); | ||
|
|
||
| if ( tasks_omitted ) { | ||
| if ( task_pool_empty ) { | ||
| // All tasks have been checked. The task pool should be in reset state. | ||
| // We just restore the bounds for the available tasks. | ||
| // TODO: Does it have sense to move them to the beginning of the task pool? | ||
| __TBB_ASSERT( is_quiescent_local_task_pool_reset(), nullptr ); | ||
| if ( result ) { | ||
| // If we have a task, it should be at H0 position. | ||
| __TBB_ASSERT( H0 == T, nullptr ); | ||
| ++H0; | ||
| } | ||
| __TBB_ASSERT( H0 <= T0, nullptr ); | ||
| if ( H0 < T0 ) { | ||
| // Restore the task pool if there are some tasks. | ||
| head.store(H0, std::memory_order_relaxed); | ||
| tail.store(T0, std::memory_order_relaxed); | ||
| // The release fence is used in publish_task_pool. | ||
| publish_task_pool(); | ||
| // Synchronize with snapshot as we published some tasks. | ||
| ed.task_disp->m_thread_data->my_arena->advertise_new_work<arena::wakeup>(); | ||
akukanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } else { | ||
| // A task has been obtained. We need to make a hole in position T. | ||
| __TBB_ASSERT( is_task_pool_published(), nullptr ); | ||
| __TBB_ASSERT( result, nullptr ); | ||
| task_pool_ptr[T] = nullptr; | ||
| tail.store(T0, std::memory_order_release); | ||
| // Synchronize with snapshot as we published some tasks. | ||
| // TODO: consider some approach not to call wakeup for each time. E.g. check if the tail reached the head. | ||
| ed.task_disp->m_thread_data->my_arena->advertise_new_work<arena::wakeup>(); | ||
| } | ||
| } while ( !result && !all_tasks_checked ); | ||
|
|
||
| if ( tasks_skipped ) { | ||
| __TBB_ASSERT( is_task_pool_published(), nullptr ); // the pool was not reset | ||
| tail.store(T0, std::memory_order_release); | ||
|
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. Do I understand correctly that we do not need to restore head here since it is a stealing thread responsibility and is done in the
Contributor
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. That is correct. Generally, H0 represents the state of the pool head as it was seen by the owner; it might get outdated at any time. The core principle therefore is that the owner only works with the tail and does not change the head. Indeed, if there was no conflict for the last task, the owner has no idea what the proper value for the head should be. And in case of a conflict the pool lock is taken and the head is re-read, and we can be sure that there is no skipped tasks beyond the head, so there is no need to change anything. Prior to the patch, there is a store of H0 to the head - but it is done at the point where the pool is temporarily quiescent, and therefore it is safe. It "optimizes" the case when the task at the head is taken while others were skipped. In the patch, the pool is not reset if tasks were skipped, as that would also mislead observers. So this optimization cannot be safely performed anymore. |
||
| } | ||
| // At this point, skipped tasks - if any - are back in the pool bounds | ||
| has_skipped_tasks.store(false, std::memory_order_relaxed); | ||
|
|
||
| __TBB_ASSERT( (std::intptr_t)tail.load(std::memory_order_relaxed) >= 0, nullptr ); | ||
| __TBB_ASSERT( result || tasks_omitted || is_quiescent_local_task_pool_reset(), nullptr ); | ||
| __TBB_ASSERT( result || tasks_skipped || is_quiescent_local_task_pool_reset(), nullptr ); | ||
| return result; | ||
| } | ||
|
|
||
|
|
@@ -153,7 +142,7 @@ d1::task* arena_slot::steal_task(arena& a, isolation_type isolation, std::size_t | |
| d1::task* result = nullptr; | ||
| std::size_t H = head.load(std::memory_order_relaxed); // mirror | ||
| std::size_t H0 = H; | ||
| bool tasks_omitted = false; | ||
| bool tasks_skipped = false; | ||
| do { | ||
| // The full fence is required to sync the store of `head` with the load of `tail` (write-read barrier) | ||
| H = ++head; | ||
|
|
@@ -181,8 +170,8 @@ d1::task* arena_slot::steal_task(arena& a, isolation_type isolation, std::size_t | |
| } | ||
| // The task cannot be executed either due to isolation or proxy constraints. | ||
| result = nullptr; | ||
| tasks_omitted = true; | ||
| } else if (!tasks_omitted) { | ||
| tasks_skipped = true; | ||
| } else if (!tasks_skipped) { | ||
| // Cleanup the task pool from holes until a task is skipped. | ||
| __TBB_ASSERT( H0 == H-1, nullptr ); | ||
| poison_pointer( victim_pool[H0] ); | ||
|
|
@@ -193,8 +182,8 @@ d1::task* arena_slot::steal_task(arena& a, isolation_type isolation, std::size_t | |
|
|
||
| // emit "task was consumed" signal | ||
| poison_pointer( victim_pool[H-1] ); | ||
| if (tasks_omitted) { | ||
| // Some proxies in the task pool have been omitted. Set the stolen task to nullptr. | ||
| if (tasks_skipped) { | ||
| // Some proxies in the task pool have been skipped. Set the stolen task to nullptr. | ||
| victim_pool[H-1] = nullptr; | ||
| // The release store synchronizes the victim_pool update(the store of nullptr). | ||
| head.store( /*dead: H = */ H0, std::memory_order_release ); | ||
|
|
@@ -206,10 +195,6 @@ d1::task* arena_slot::steal_task(arena& a, isolation_type isolation, std::size_t | |
| __TBB_cl_evict(&victim_slot.head); | ||
| __TBB_cl_evict(&victim_slot.tail); | ||
| #endif | ||
| if (tasks_omitted) { | ||
| // Synchronize with snapshot as the head and tail can be bumped which can falsely trigger EMPTY state | ||
| a.advertise_new_work<arena::wakeup>(); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.