P3 · SIMD and tasks¶
Render a 1200 × 800 Mandelbrot image using SIMD within each core and tasks across cores.
Part 1 · Single-core SIMD¶
The avx2-i32x8 compiler target sets programCount = 8: eight program instances form a gang. foreach distributes pixels among those instances.
Eight-wide SIMD gives an ideal speedup near 8× under the handout's model.
| View | Serial (ms) | ISPC (ms) | Speedup |
|---|---|---|---|
| 1 | 121.300 | 34.424 | 3.52× |
| 2 | 68.568 | 24.183 | 2.84× |
Pixels in a vector can require different iteration counts. Finished lanes are masked off while the slowest lane continues. This is particularly costly near fractal boundaries, where nearby pixels can have very different escape counts. Mask handling and loop control add overhead as well.
Part 2 · ISPC tasks¶
2.1 Measure the two-task version¶
launch creates tasks for the runtime's worker threads. Each task executes as a gang and processes its region in batches of pixels. The original two-task version assigns 400 rows to each task.
For view 1, it takes 18.391 ms: 6.62× over serial and 1.87× over single-core ISPC. Two tasks leave most of the local CPU's cores idle.
2.2 Increase the task count¶
The implementation uses 200 tasks, each covering four rows. More tasks provide additional parallel work and let workers take another region when they finish.
launch[200]
|
v
[task 0] [task 1] ... [task 199] four rows per task
|
| runtime schedules tasks on worker threads
v
One running task
+-----------------------------------------+
| Gang: [0] [1] [2] [3] [4] [5] [6] [7] | programCount = 8
| foreach -> successive batches of pixels |
+-----------------------------------------+
A task-count sweep gives:
| Tasks | Rows per task | View 1 time (ms) | Speedup over serial |
|---|---|---|---|
| 2 | 400 | 18.391 | 6.62× |
| 8 | 100 | 9.425 | 12.90× |
| 32 | 25 | 3.581 | 33.85× |
| 200 | 4 | 2.752 | 44.08× |
| 400 | 2 | 2.770 | 43.81× |
| 800 | 1 | 2.746 | 44.24× |
View 1 reaches a plateau around 200 tasks. Task size balances load distribution against scheduling overhead.
Results for the current 200-task version:
| View | Task ISPC (ms) | Serial / tasks | ISPC / tasks |
|---|---|---|---|
| 1 | 2.752 | 44.08× | 12.51× |
| 2 | 2.185 | 31.38× | 11.07× |
Both ISPC outputs match the serial image. The selected task counts divide all 800 rows evenly.
The handout's 32× target is exceeded on view 1. View 2 improves from 2.185 ms at 200 tasks to 1.970 ms at 800 tasks, giving 34.80×. The smaller tasks improve scheduling on this 20-logical-CPU machine; the useful task count depends on both the machine and the image.
2.3 Extra credit: tasks and threads¶
A std::thread has an OS-scheduled execution context and its own stack. ISPC tasks share a pool of worker threads. Ten thousand tasks can be queued and processed by that pool; ten thousand OS threads incur much larger creation, memory, and scheduling costs.
join waits for a particular thread. ISPC sync waits for tasks launched by the current function, and function return also synchronizes them. See the ISPC tasking model.
2.4 Why separate foreach and launch?¶
SIMD and multicore scheduling operate at different granularities. Keeping them separate lets a short loop use SIMD with little overhead and a larger workload be divided into tasks. It also lets an application use ISPC inside an existing thread pool. A higher-level parallel-loop construct could combine both mechanisms.