feat(linear-static-3d-euler-beam): step 17 - parallel-for-tbb

This commit is contained in:
KOKO\Mimi
2026-08-09 18:52:35 +09:00
parent cfdac70756
commit 2f5e737fa1
6 changed files with 211 additions and 0 deletions
@@ -630,3 +630,54 @@
| FIX1-VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | 39 total tests discovered, including the same ten EulerBeam3D names | | FIX1-VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | 39 total tests discovered, including the same ten EulerBeam3D names |
| FIX1-VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | 39/39 passed | | FIX1-VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | 39/39 passed |
| FIX1-VERIFY-scans | fail-on-match numerical invariant, dependency, scope, exact-test-count, reference, phase-index, and diff/whitespace scans | 0 | derived rigidity/coefficient and fail-closed checks present; tests 10; leaks 0; reference and phase index unchanged; diff clean | | FIX1-VERIFY-scans | fail-on-match numerical invariant, dependency, scope, exact-test-count, reference, phase-index, and diff/whitespace scans | 0 | derived rigidity/coefficient and fail-closed checks present; tests 10; leaks 0; reference and phase index unchanged; diff clean |
## Step 17 — parallel-for-tbb
- task_id: `TASK-17`
- status: `completed`
- changed_files: `include/fesa/assembly/parallel_for.hpp`,
`src/fesa/assembly/parallel_for.cpp`,
`tests/unit/assembly/parallel_for_test.cpp`, `src/fesa/CMakeLists.txt`,
`tests/CMakeLists.txt`,
`docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md`,
`phases/linear-static-3d-euler-beam/index.json`,
`.superpowers/sdd/linear-static-3d-euler-beam/task-17-report.md`
- requirement_ids: `FESA-REQ-LS3DEB-024`, `FESA-REQ-LS3DEB-025`,
`FESA-REQ-LS3DEB-034`
- test_ids: `T17-PFOR-001`, `T17-PFOR-002`, `T17-PFOR-003`
| stage | exact command | exit_code | expected_or_observed_result | evidence_tail |
| --- | --- | ---: | --- | --- |
| RED-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 1 | Exactly three planned tests were registered before production and the ParallelFor public API was absent | MSVC C1083 reported missing `fesa/assembly/parallel_for.hpp` from `parallel_for_test.cpp` after successful CMake regeneration |
| GREEN-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 0 | Minimal serial/oneTBB adapters, three tests, solver library, and unit executable compile and link | `parallel_for.cpp`, `parallel_for_test.cpp`, `fesa_solver.lib`, and `fesa_unit_tests.exe` built without a FESA warning under `/W4 /WX`; oneTBB runtime staged for discovery |
| GREEN-test | `ctest --test-dir .harness/build -C Debug -R ParallelFor --output-on-failure` | 0 | Zero/one/many exact-once, stable indexed output, and exception propagation pass | 3/3 exact `ParallelFor` tests passed |
| VERIFY-configure | `cmake -S . -B .harness/build -A x64 -DFESA_GTEST_SOURCE_DIR=C:/git/googletest "-DMKL_DIR=C:/Program Files (x86)/Intel/oneAPI/mkl/2026.1/lib/cmake/mkl" "-DTBB_DIR=C:/Program Files (x86)/Intel/oneAPI/tbb/2023.1/lib/cmake/tbb" "-DHDF5_DIR=C:/Program Files/HDF_Group/HDF5/2.1.1/cmake"` | 0 | Approved explicit-dependency MSVC x64 build tree generates | Windows SDK, oneMKL 2026.1, oneTBB, and HDF5 resolved; configure and generate completed |
| VERIFY-build | `cmake --build .harness/build --config Debug` | 0 | Full Debug build passes without a new FESA warning | `fesa_solver.lib` and `fesa_unit_tests.exe` built under `/W4 /WX` |
| VERIFY-targeted | `ctest --test-dir .harness/build -C Debug -R ParallelFor --output-on-failure` | 0 | Focused Step 17 suite remains green | 3/3 exact `ParallelFor` tests passed |
| VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | CTest discovers the accumulated suite and all three exact ParallelFor names | 42 tests discovered, including 3 `ParallelFor` tests, with feature/unit labels |
| VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | Full accumulated C++ suite has zero failures | 42/42 tests passed |
| VERIFY-contract-scans | Public oneTBB leak, private include, forbidden scheduler/assembly scope, exact-test-count, and CMake-registration scans using fail-on-match/count `rg` wrappers | 0 | The backend-free adapter implements only independent indexed work and retains the default caller-scoped scheduler policy | public TBB leaks 0; forbidden global/sparse/reduction policy 0; private TBB includes 1; tests 3; registrations 1/1/1 |
| VERIFY-diff | `git diff --check` plus trailing-whitespace scan over the three new files | 0 | Tracked and untracked Step 17 files have no whitespace errors | Diff check exit 0; new-file trailing whitespace matches 0 |
| VERIFY-reference | `git diff --exit-code -- reference/`; `git status --short -- reference/` | 0 | Approved legacy reference artifacts remain read-only and unchanged | Reference diff exit 0 and reference status empty |
- contract_checks: `ParallelFor`, `SerialParallelFor`, and `TbbParallelFor`
expose only `std::size_t` and `std::function` in the public header. oneTBB
headers/types remain private to `parallel_for.cpp`, and the normalized
`Fesa::TBB` dependency is private to `fesa_solver`.
- contract_checks: both implementations execute `[0,count)` exactly once and
make `count==0` a no-op. Deterministic result ordering is caller-owned:
bodies write only their index-addressed slot, and this adapter performs no
global sparse write, COO/CSR reduction, mutex, or atomic accumulation.
- contract_checks: serial and oneTBB execution preserve the original body
exception type and message. oneTBB cancellation may allow already-running
indexed work to finish; callers receive the rethrown exception and must not
infer a deterministic partial-completion count.
- contract_checks: the adapter uses oneTBB's default caller-scoped scheduler;
it creates no process-wide thread setting, `global_control`, or task arena,
and does not override later MKL/TBB oversubscription coordination.
- generated_evidence: `.harness/build/src/fesa/Debug/fesa_solver.lib`,
`.harness/build/tests/Debug/fesa_unit_tests.exe`
- reference_diff: unchanged; `git diff --exit-code -- reference/` exit 0
- handoff: Step 18 can inject either backend for independent element-local
computation into caller-owned stable element-order slots before its separate
deterministic COO-to-CSR reduction.
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <cstddef>
#include <functional>
namespace fesa {
// Executes independent index-addressed work without exposing the backend.
// Callers own output storage and must confine each invocation to its index.
class ParallelFor {
public:
virtual ~ParallelFor() = default;
virtual void execute(
std::size_t count,
const std::function<void(std::size_t)>& body) const = 0;
};
class SerialParallelFor final : public ParallelFor {
public:
void execute(
std::size_t count,
const std::function<void(std::size_t)>& body) const override;
};
class TbbParallelFor final : public ParallelFor {
public:
void execute(
std::size_t count,
const std::function<void(std::size_t)>& body) const override;
};
} // namespace fesa
+2
View File
@@ -3,6 +3,7 @@ add_library(
STATIC STATIC
analysis/analysis_model.cpp analysis/analysis_model.cpp
analysis/analysis_state.cpp analysis/analysis_state.cpp
assembly/parallel_for.cpp
build_info.cpp build_info.cpp
core/diagnostic.cpp core/diagnostic.cpp
core/status.cpp core/status.cpp
@@ -25,6 +26,7 @@ target_link_libraries(
fesa_solver fesa_solver
PRIVATE PRIVATE
Fesa::MKL Fesa::MKL
Fesa::TBB
) )
# Product warnings are strict without imposing FESA policy on external targets. # Product warnings are strict without imposing FESA policy on external targets.
+30
View File
@@ -0,0 +1,30 @@
#include "fesa/assembly/parallel_for.hpp"
#include <oneapi/tbb/parallel_for.h>
namespace fesa {
void SerialParallelFor::execute(
std::size_t count,
const std::function<void(std::size_t)>& body) const {
for (std::size_t index = 0; index < count; ++index) {
body(index);
}
}
void TbbParallelFor::execute(
std::size_t count,
const std::function<void(std::size_t)>& body) const {
if (count == 0U) {
return;
}
// Use oneTBB's caller-scoped scheduler policy. This adapter does not set
// process-wide concurrency or override the later MKL/TBB oversubscription
// policy. A body exception cancels sibling tasks and is rethrown; work
// already running during cancellation may still finish its indexed slot.
oneapi::tbb::parallel_for(
std::size_t{0}, count, [&body](std::size_t index) { body(index); });
}
} // namespace fesa
+4
View File
@@ -5,6 +5,7 @@ add_executable(
unit/build_info_test.cpp unit/build_info_test.cpp
unit/analysis/analysis_model_test.cpp unit/analysis/analysis_model_test.cpp
unit/analysis/analysis_state_test.cpp unit/analysis/analysis_state_test.cpp
unit/assembly/parallel_for_test.cpp
unit/core/diagnostic_test.cpp unit/core/diagnostic_test.cpp
unit/core/source_identity_test.cpp unit/core/source_identity_test.cpp
unit/core/status_test.cpp unit/core/status_test.cpp
@@ -45,6 +46,9 @@ if(WIN32)
add_custom_command( add_custom_command(
TARGET fesa_unit_tests TARGET fesa_unit_tests
POST_BUILD POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<TARGET_FILE:TBB::tbb>"
"$<TARGET_FILE_DIR:fesa_unit_tests>"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<TARGET_FILE:MKL::mkl_intel_thread>" "$<TARGET_FILE:MKL::mkl_intel_thread>"
"$<TARGET_FILE_DIR:fesa_unit_tests>" "$<TARGET_FILE_DIR:fesa_unit_tests>"
+92
View File
@@ -0,0 +1,92 @@
#include "fesa/assembly/parallel_for.hpp"
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
#include <functional>
#include <stdexcept>
#include <string>
#include <vector>
namespace fesa {
namespace {
class ParallelForBodyError final : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
std::array<std::reference_wrapper<const ParallelFor>, 2> parallelForBackends(
const SerialParallelFor& serial,
const TbbParallelFor& tbb) {
return {std::cref(serial), std::cref(tbb)};
}
TEST(ParallelFor, ZeroOneManyExecuteExactlyOnce) {
const SerialParallelFor serial;
const TbbParallelFor tbb;
for (const ParallelFor& parallelFor : parallelForBackends(serial, tbb)) {
bool zeroBodyCalled = false;
parallelFor.execute(0U, [&zeroBodyCalled](std::size_t) {
zeroBodyCalled = true;
});
EXPECT_FALSE(zeroBodyCalled);
for (const std::size_t count : {1U, 257U}) {
std::vector<int> visits(count, 0);
parallelFor.execute(count, [&visits](std::size_t index) {
++visits[index];
});
EXPECT_EQ(visits, std::vector<int>(count, 1));
}
}
}
TEST(ParallelFor, SerialAndTbbProduceStableIndexedOutput) {
constexpr std::size_t count = 1024U;
std::vector<std::size_t> serialOutput(count, 0U);
std::vector<std::size_t> tbbOutput(count, 0U);
const auto valueForIndex = [](std::size_t index) {
return (index + 17U) * (index + 3U);
};
const SerialParallelFor serial;
serial.execute(count, [&serialOutput, &valueForIndex](std::size_t index) {
serialOutput[index] = valueForIndex(index);
});
const TbbParallelFor tbb;
tbb.execute(count, [&tbbOutput, &valueForIndex](std::size_t index) {
tbbOutput[index] = valueForIndex(index);
});
EXPECT_EQ(tbbOutput, serialOutput);
for (std::size_t index = 0; index < count; ++index) {
EXPECT_EQ(tbbOutput[index], valueForIndex(index));
}
}
TEST(ParallelFor, PropagatesBodyExceptionByContract) {
const SerialParallelFor serial;
const TbbParallelFor tbb;
for (const ParallelFor& parallelFor : parallelForBackends(serial, tbb)) {
try {
// Every iteration throws the same value so the assertion is independent
// of which oneTBB task reports the cancellation-triggering exception.
parallelFor.execute(64U, [](std::size_t) {
throw ParallelForBodyError{"parallel-for-body-failure"};
});
ADD_FAILURE() << "ParallelFor swallowed the body exception.";
} catch (const ParallelForBodyError& error) {
EXPECT_EQ(std::string{error.what()}, "parallel-for-body-failure");
} catch (...) {
ADD_FAILURE() << "ParallelFor changed the body exception type.";
}
}
}
} // namespace
} // namespace fesa