diff --git a/doc/main/reference/examples/task_group_extensions_bypassing.cpp b/doc/main/reference/examples/task_group_extensions_bypassing.cpp new file mode 100644 index 0000000000..3d2beb0b4b --- /dev/null +++ b/doc/main/reference/examples/task_group_extensions_bypassing.cpp @@ -0,0 +1,80 @@ +/* + 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. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include + +static constexpr std::size_t serial_threshold = 16; + +/*begin_task_group_extensions_bypassing_example*/ +#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 +#include "oneapi/tbb/task_group.h" + +template +struct for_task { + tbb::task_handle operator()() const { + tbb::task_handle next_task; + + auto size = end - begin; + if (size < serial_threshold) { + // Execute the work serially + for (Iterator it = begin; it != end; ++it) { + f(*it); + } + } else { + // Enough work to split the range + Iterator middle = begin + size / 2; + + // Submit the right subtask for execution + tg.run(for_task{middle, end, f, tg}); + + // Bypass the left subtask + next_task = tg.defer(for_task{begin, middle, f, tg}); + } + return next_task; + } + + Iterator begin; + Iterator end; + Function f; + tbb::task_group& tg; +}; // struct for_task + +template +void par_for_each(RandomAccessIterator begin, RandomAccessIterator end, Function f) { + tbb::task_group tg; + // Run the root task + tg.run_and_wait(for_task{begin, end, std::move(f), tg}); +} +/*end_task_group_extensions_bypassing_example*/ + +int main() { + constexpr std::size_t N = 1000; + + std::size_t array[N]; + + par_for_each(array, array + N, [](std::size_t& item) { + item = 42; + }); + + for (std::size_t i = 0; i < N; ++i) { + if (array[i] != 42) { + std::cerr << "Error in " << i << "index" << std::endl; + return 1; + } + } +} diff --git a/doc/main/reference/examples/task_group_extensions_reduction.cpp b/doc/main/reference/examples/task_group_extensions_reduction.cpp new file mode 100644 index 0000000000..e1126691ab --- /dev/null +++ b/doc/main/reference/examples/task_group_extensions_reduction.cpp @@ -0,0 +1,98 @@ +/* + 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. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include + +static constexpr std::size_t serial_threshold = 16; + +/*begin_task_group_extensions_reduction_example*/ +#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 +#include "oneapi/tbb/task_group.h" + +struct reduce_task { + + struct join_task { + void operator()() const { + result = *left + *right; + } + + std::size_t& result; + std::unique_ptr left; + std::unique_ptr right; + }; + + tbb::task_handle operator()() const { + tbb::task_handle next_task; + + std::size_t size = end - begin; + if (size < serial_threshold) { + // Perform serial reduction + for (std::size_t i = begin; i < end; ++i) { + result += i; + } + } else { + // The range is too large to process directly + // Divide it into smaller segments for parallel execution + std::size_t middle = begin + size / 2; + + auto left_result = std::make_unique(0); + auto right_result = std::make_unique(0); + + + tbb::task_handle left_leaf = tg.defer(reduce_task{begin, middle, *left_result, tg}); + tbb::task_handle right_leaf = tg.defer(reduce_task{middle, end, *right_result, tg}); + + tbb::task_handle join = tg.defer(join_task{result, std::move(left_result), std::move(right_result)}); + + tbb::task_group::set_task_order(left_leaf, join); + tbb::task_group::set_task_order(right_leaf, join); + + tbb::task_group::transfer_this_task_completion_to(join); + + // Save the left leaf for further bypassing + next_task = std::move(left_leaf); + + tg.run(std::move(right_leaf)); + tg.run(std::move(join)); + } + + return next_task; + } + + std::size_t begin; + std::size_t end; + std::size_t& result; + tbb::task_group& tg; +}; + +std::size_t calculate_parallel_sum(std::size_t begin, std::size_t end) { + tbb::task_group tg; + + std::size_t reduce_result = 0; + tg.run_and_wait(reduce_task{begin, end, reduce_result, tg}); + + return reduce_result; +} +/*end_task_group_extensions_reduction_example*/ + +int main() { + constexpr std::size_t N = 10000; + std::size_t serial_sum = N * (N - 1) / 2; + std::size_t parallel_sum = calculate_parallel_sum(0, N); + + if (serial_sum != parallel_sum) std::cerr << "Incorrect reduction result" << std::endl; +} diff --git a/doc/main/reference/task_group_bypass_support.rst b/doc/main/reference/task_group_bypass_support.rst new file mode 100644 index 0000000000..41e8198ff6 --- /dev/null +++ b/doc/main/reference/task_group_bypass_support.rst @@ -0,0 +1,100 @@ +.. _task_group_bypass_support: + +Task Bypass Support for ``task_group`` +====================================== + +.. note:: + To enable this extension, define the ``TBB_PREVIEW_TASK_GROUP_EXTENSIONS`` macro with a value of ``1``. + +.. contents:: + :local: + :depth: 2 + +Description +*********** + +The |full_name| implementation extends the requirements for user-provided function object from +`tbb::task_group specification `_ +to allow them to return a ``task_handle`` object. + +`Task Bypassing <../tbb_userguide/Task_Scheduler_Bypass.html>`_ allows developers to reduce task scheduling overhead by providing a hint about +which task should be executed next. + +Execution of the deferred task owned by a returned ``task_handle`` is not guaranteed to occur immediately, nor to be performed by the same thread. + +.. code:: cpp + + tbb::task_handle task_body() { + tbb::task_handle next_task = group.defer(next_task_body); + return next_task; + } + +API +*** + +Header +------ + +.. code:: cpp + + #define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 + #include + +Synopsis +-------- + +.. code:: cpp + + namespace oneapi { + namespace tbb { + class task_group { + public: + // Only the requirements for the return type of function F are changed + template + task_handle defer(F&& f); + + // Only the requirements for the return type of function F are changed + template + task_group_status run_and_wait(const F& f); + + // Only the requirements for the return type of function F are changed + template + void run(F&& f); + }; // class task_group + } // namespace tbb + } // namespace oneapi + +Member Functions +---------------- + +.. code:: cpp + + template + task_handle defer(F&& f); + + template + task_group_status run_and_wait(const F& f); + + template + void run(F&& f); + +The ``F`` type must meet the *Function Objects* requirements described in the [function.objects] section of the ISO C++ Standard. + +.. admonition:: Extension + + ``F`` may return a ``task_handle`` object. If the returned handle is non-empty and owns a task without dependencies, it serves as an optimization hint + for a task that could be executed next. + + The returned ``task_handle`` must not be explicitly submitted with ``task_group::run`` or another submission function, otherwise, the behavior is undefined. + + If the returned handle was created by a ``task_group`` other than ``*this``, the behavior is undefined. + +Example +------- + +The example below demonstrates how to process a sequence in parallel using ``task_group`` and the divide-and-conquer pattern. + +.. literalinclude:: ./examples/task_group_extensions_bypassing.cpp + :language: c++ + :start-after: /*begin_task_group_extensions_bypassing_example*/ + :end-before: /*end_task_group_extensions_bypassing_example*/ diff --git a/doc/main/reference/task_group_dynamic_dependencies.rst b/doc/main/reference/task_group_dynamic_dependencies.rst new file mode 100644 index 0000000000..9d49e35533 --- /dev/null +++ b/doc/main/reference/task_group_dynamic_dependencies.rst @@ -0,0 +1,379 @@ +.. _task_group_dynamic_dependencies: + +``task_group`` Dynamic Dependencies +=================================== + +.. note:: + To enable this extension, define the ``TBB_PREVIEW_TASK_GROUP_EXTENSIONS`` macro with a value of ``1``. + +.. contents:: + :local: + :depth: 2 + +Description +*********** + +The |full_name| implementation extends the +`tbb::task_group specification `_ +with an API for defining predecessor-successor relationships between tasks, +such that a successor task can begin execution only after all of its predecessors are completed. + +An **unsubmitted** task is one that has not been submitted for execution. + +A **submitted** task is one that has been submitted for execution, such as by passing a ``task_handle`` to ``task_group::run``. + +A non-empty ``task_handle`` object represents an unsubmitted task, while a ``task_completion_handle`` can represent any task. + +Both submitted and unsubmitted tasks can serve as predecessors. However, only unsubmitted tasks may be used as successors. + +.. code:: cpp + + tbb::task_handle task = tg.defer(task_body); + // task is unsubmitted + + tbb::task_completion_handle comp_handle = task; + // task is unsubmitted + // both task_handle and task_completion_handle represent the task + + tg.run(std::move(task)); + // task is submitted + // task_handle is empty, task_completion_handle represents the task + + // At any stage, comp_handle may be used to add successors to the task + +The ``tbb::task_group::set_task_order(pred, succ)`` function establishes a dependency such that ``succ`` cannot begin execution until ``pred`` has completed. + +.. code:: cpp + + tbb::task_handle predecessor = tg.defer(pred_body); + tbb::task_handle successor = tg.defer(succ_body); + + tbb::task_group::set_task_order(predecessor, successor); + +The ``tbb::task_group::transfer_this_task_completion_to`` function allows transferring the completion of the currently executing task to another task. +This function must be invoked from within the task body. All successors of the currently executing task will execute only after the task receiving the completion has finished. + +.. code:: cpp + + tbb::task_handle t = tg.defer([&tg] { + tbb::task_handle comp_receiver = tg.defer(receiver_body); + tbb::task_group::transfer_this_task_completion_to(comp_receiver); + tg.run(std::move(comp_receiver)); + }); + + tbb::task_handle succ = tg.defer(succ_body); + + tbb::task_group::set_task_order(t, succ); + // Since t transfers its completion to comp_receiver, + // succ_body will execute after receiver_body + +API +*** + +Header +------ + +.. code:: cpp + + #define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 + #include + #include + +Synopsis +-------- + +.. code:: cpp + + // synopsis + namespace oneapi { + namespace tbb { + class task_handle { + public: + // Only the requirements for destroyed object are changed + ~task_handle(); + }; + + class task_completion_handle { + public: + task_completion_handle(); + + task_completion_handle(const task_handle& handle); + task_completion_handle(const task_completion_handle& other); + task_completion_handle(task_completion_handle&& other); + + ~task_completion_handle(); + + task_completion_handle& operator=(const task_handle& handle); + task_completion_handle& operator=(const task_completion_handle& other); + task_completion_handle& operator=(task_completion_handle&& other); + + explicit operator bool() const noexcept; + + friend bool operator==(const task_completion_handle& lhs, + const task_completion_handle& rhs) noexcept; + friend bool operator!=(const task_completion_handle& lhs, + const task_completion_handle& rhs) noexcept; + + friend bool operator==(const task_completion_handle& t, std::nullptr_t) noexcept; + friend bool operator!=(const task_completion_handle& t, std::nullptr_t) noexcept; + + friend bool operator==(std::nullptr_t, const task_completion_handle& t) noexcept; + friend bool operator!=(std::nullptr_t, const task_completion_handle& t) noexcept; + }; // class task_completion_handle + + class task_group { + // Only the behavior in case of dependent tasks is changed + void run(task_handle&& handle); + + static void set_task_order(task_handle& pred, task_handle& succ); + static void set_task_order(task_completion_handle& pred, task_handle& succ); + + static void transfer_this_task_completion_to(task_handle& handle); + }; + } // namespace tbb + } // namespace oneapi + +.. code:: cpp + + // // synopsis + namespace oneapi { + namespace tbb { + class task_arena { + // Only the behavior in case of dependent tasks is changed + void enqueue(task_handle&& handle); + }; // class task_arena + + namespace this_task_arena { + // Only the behavior in case of dependent tasks is changed + void enqueue(task_handle&& handle); + } // namespace this_task_arena + } // namespace tbb + } // namespace oneapi + +``task_completion_handle`` Class +-------------------------------- + +Constructors +~~~~~~~~~~~~ + +.. code:: cpp + + task_completion_handle(); + +Constructs an empty ``task_completion_handle`` that does not refer to any task. + +.. code:: cpp + + task_completion_handle(const task_handle& handle); + +Constructs a ``task_completion_handle`` that refers to the task associated with ``handle``. +If ``handle`` is empty, the behavior is undefined. + +.. code:: cpp + + task_completion_handle(const task_completion_handle& other); + +Copies ``other`` into ``*this``. After the copy, both ``*this`` and ``other`` refer to the same task. + +.. code:: cpp + + task_completion_handle(task_completion_handle&& other); + +Moves ``other`` into ``*this``. After the move, ``*this`` refers to the task previously referenced by ``other``, which is left empty. + +Destructors +~~~~~~~~~~~ + +.. code:: cpp + + ~task_completion_handle(); + +Destroys the ``task_completion_handle``. + +Assignment +~~~~~~~~~~ + +.. code:: cpp + + task_completion_handle& operator=(const task_handle& handle); + +Replaces the task referenced by ``*this`` with the task associated with ``handle``. +If ``handle`` is empty, the behavior is undefined. + +*Returns*: a reference to ``*this``. + +.. code:: cpp + + task_completion_handle& operator=(const task_completion_handle& other); + +Performs copy assignment from ``other`` to ``*this``. After the assignment, both refer to the same task. + +*Returns*: a reference to ``*this``. + +.. code:: cpp + + task_completion_handle& operator=(task_completion_handle&& other); + +Performs move assignment from ``other`` to ``*this``. After the move, ``*this`` refers to the task previously referenced by ``other``, which is left empty. + +*Returns*: a reference to ``*this``. + +Observers +~~~~~~~~~ + +.. code:: cpp + + explicit operator bool() const noexcept; + +*Returns*: ``true`` if ``*this`` references a task; otherwise, ``false``. + +Comparison +~~~~~~~~~~ + +.. code:: cpp + + bool operator==(const task_completion_handle& lhs, const task_completion_handle& rhs) noexcept; + +*Returns*: ``true`` if ``lhs`` and ``rhs`` reference the same task; otherwise, ``false``. + +.. code:: cpp + + bool operator!=(const task_completion_handle& lhs, const task_completion_handle& rhs) noexcept; + +Equivalent to ``!(lhs == rhs)``. + +.. code:: cpp + + bool operator==(const task_completion_handle& t, std::nullptr_t) noexcept; + bool operator==(std::nullptr_t, const task_completion_handle& t) noexcept; + +*Returns*: ``true`` if ``t`` does not reference any task; otherwise, ``false``. + +.. code:: cpp + + bool operator!=(const task_completion_handle& t, std::nullptr_t) noexcept; + bool operator!=(std::nullptr_t, const task_completion_handle& t) noexcept; + +Equivalent to ``!(t == nullptr)``. + +Member Functions of ``task_handle`` Class +----------------------------------------- + +.. code:: cpp + + ~task_handle(); + +Destroys the ``task_handle`` object and associated task if it exists. + +.. admonition:: Extension + + If the associated task is involved in a predecessor-successor relationship, the behavior is undefined. + +Member Functions of ``task_group`` Class +---------------------------------------- + +.. code:: cpp + + void run(task_handle&& h); + +Schedules the task object pointed by ``h`` for the execution. + +.. admonition:: Extension + + If the task associated with ``h`` has predecessors, scheduling the task execution is postponed until all + of the predecessors have completed, while the function returns immediately. + +.. note:: + + The failure to satisfy the following conditions leads to undefined behavior: + + * ``h`` is not empty. + * ``*this`` is the same ``task_group`` that ``h`` is created with. + +.. code:: cpp + + static void set_task_order(task_handle& pred, task_handle& succ); + static void set_task_order(task_completion_handle& pred, task_handle& succ); + +Registers the task associated with ``pred`` as a predecessor that must complete before the task associated with ``succ`` can begin execution. + +It is thread-safe to concurrently add multiple predecessors to a single successor and to register the same predecessor with multiple successors. + +It is thread-safe to concurrently add successors to both the task transferring its completion and the task receiving the completion. + +It is thread-safe to concurrently add a successor to a ``task_completion_handle`` while the ``task_handle`` associated with the same task is being run. + +The behavior is undefined in the following cases: + +* Either ``pred`` or ``succ`` is empty. +* The tasks referred by ``pred`` and ``succ`` belong to different ``task_group`` instances. +* The task referred by ``task_completion_handle`` was destroyed without being submitted for execution. + +.. code:: cpp + + static void transfer_this_task_completion_to(task_handle& handle); + +Transfers the completion of the currently executing task to the task associated with ``handle``. + +After the transfer, the successors of the currently executing task will be reassigned to the task associated with ``handle``. + +It is thread-safe to transfer successors to the task while concurrently adding successors to it or to the currently executing task. + +The behavior is undefined in the following cases: + +* ``handle`` is empty. +* The function is called outside the body of a ``task_group`` task. +* The function is called for the task whose completion has already been transferred. +* The currently executing task and the task associated with ``handle`` belong to different ``task_group`` instances. + +Member Functions of ``task_arena`` Class +---------------------------------------- + +.. code:: cpp + + void enqueue(task_handle&& h); + +Enqueues a task owned by ``h`` into the ``task_arena`` for processing. + +.. admonition:: Extension + + If the task associated with ``h`` has predecessors, scheduling the task execution is postponed until all + of the predecessors have completed, while the function returns immediately. + +The behavior of this function is identical to the generic version (``template void task_arena::enqueue(F&& f)``), +except parameter type. + +.. note:: + + ``h`` should not be empty to avoid an undefined behavior. + +``this_task_arena`` Namespace +----------------------------- + +.. code:: cpp + + void enqueue(task_handle&& h); + +Enqueues a task owned by ``h`` into the ``task_arena`` that is currently used by the calling thread. + +.. admonition:: Extension + + If the task associated with ``h`` has predecessors, scheduling the task execution is postponed until all + of the predecessors have completed, while the function returns immediately. + +The behavior of this function is identical to the generic version (``template void task_arena::enqueue(F&& f)``), +except parameter type. + +.. note:: + + ``h`` should not be empty to avoid an undefined behavior. + +Example +------- + +The following example demonstrates how to perform parallel reduction over a range using the described API. + +.. literalinclude:: ./examples/task_group_extensions_reduction.cpp + :language: c++ + :start-after: /*begin_task_group_extensions_reduction_example*/ + :end-before: /*end_task_group_extensions_reduction_example*/ diff --git a/doc/main/reference/task_group_extensions.rst b/doc/main/reference/task_group_extensions.rst index 47795f9574..b879c95fbb 100644 --- a/doc/main/reference/task_group_extensions.rst +++ b/doc/main/reference/task_group_extensions.rst @@ -3,84 +3,17 @@ task_group extensions ===================== -.. note:: - To enable these extensions, set the ``TBB_PREVIEW_TASK_GROUP_EXTENSIONS`` macro to 1. - -.. contents:: - :local: - :depth: 1 - -Description -*********** - -|full_name| implementation extends the `tbb::task_group specification `_ with the requirements for a user-provided function object. - - -API -*** - -Header ------- - -.. code:: cpp - - #include - -Synopsis --------- - -.. code:: cpp - - namespace oneapi { - namespace tbb { - - class task_group { - public: - - //only the requirements for the return type of function F are changed - template - task_handle defer(F&& f); - - //only the requirements for the return type of function F are changed - template - task_group_status run_and_wait(const F& f); - - //only the requirements for the return type of function F are changed - template - void run(F&& f); - }; - - } // namespace tbb - } // namespace oneapi - - - -Member Functions ----------------- - -.. cpp:function:: template task_handle defer(F&& f) - -As an optimization hint, ``F`` might return a ``task_handle``, which task object can be executed next. +This section documents ``task_group`` API extensions for advanced use cases. .. note:: - The ``task_handle`` returned by the function must be created using ``*this`` ``task_group``. That is, the one for which the run method is called, otherwise it is undefined behavior. + To enable these extensions, define the ``TBB_PREVIEW_TASK_GROUP_EXTENSIONS`` macro with a value of ``1``. -.. cpp:function:: template task_group_status run_and_wait(const F& f) +.. toctree:: + :titlesonly: -As an optimization hint, ``F`` might return a ``task_handle``, which task object can be executed next. + task_group_bypass_support + task_group_dynamic_dependencies -.. note:: - The ``task_handle`` returned by the function must be created using ``*this`` ``task_group``. That is, the one for which the run method is called, otherwise it is undefined behavior. - - -.. cpp:function:: template void run(F&& f) - -As an optimization hint, ``F`` might return a ``task_handle``, which task object can be executed next. - -.. note:: - The ``task_handle`` returned by the function must be created with ``*this`` ``task_group``. It means, with the one for which run method is called, otherwise it is an undefined behavior. - - .. rubric:: See also * `oneapi::tbb::task_group specification `_