| Raise the isolate ceiling to 32, and add the sweep that justifies it 59a731a nandithebull 10h ago | 1 | // Finds where band parallelism stops paying on this machine. |
| 2 | // |
| 3 | // just sweep |
| 4 | // |
| 5 | // The interesting result is that the best tile count is well above the core |
| 6 | // count. Bands cost very different amounts — rows crossing the set's interior |
| 7 | // run the full iteration cap, rows in open space escape almost immediately — |
| 8 | // and a frame is not finished until its slowest band is. Cutting finer lets a |
| 9 | // free core start the next small band instead of idling, so over-decomposing |
| 10 | // wins even though it cannot add parallelism. |
| 11 | import 'dart:io'; |
| 12 | |
| Replace V with Nim 472eb49 nandithebull 8h ago | 13 | import 'package:nimflutter_ffi/nimflutter_ffi.dart' as v; |
| Raise the isolate ceiling to 32, and add the sweep that justifies it 59a731a nandithebull 10h ago | 14 | |
| 15 | const _view = v.FractalView(width: 800, height: 600, maxIter: 500); |
| 16 | |
| 17 | Future<void> main() async { |
| 18 | // Warm the library and the code paths before timing anything. |
| 19 | v.render(const v.FractalView(width: 64, height: 64, maxIter: 50)); |
| 20 | |
| 21 | print('${_view.width}x${_view.height}, maxIter ${_view.maxIter}, ' |
| 22 | '${Platform.numberOfProcessors} logical cores\n'); |
| 23 | |
| 24 | final serial = _best(() => v.render(_view)); |
| 25 | print('tiles best ms speedup'); |
| 26 | print('${"serial".padRight(8)} ${serial.toString().padRight(9)} 1.00x'); |
| 27 | |
| 28 | for (final tiles in [1, 2, 4, 8, 12, 16, 24, 32, 48, 64]) { |
| 29 | final ms = await _bestAsync(() => v.renderParallel(_view, tiles: tiles)); |
| 30 | print('${tiles.toString().padRight(8)} ${ms.toString().padRight(9)} ' |
| 31 | '${(serial / ms).toStringAsFixed(2)}x'); |
| 32 | } |
| 33 | |
| 34 | print('\nwhy more tiles than cores helps — per-band cost spread:'); |
| 35 | for (final tiles in [8, 32]) { |
| 36 | _imbalance(tiles); |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | /// Times each band of a [tiles]-way split serially, so the numbers describe |
| 41 | /// the work itself rather than how it happened to be scheduled. |
| 42 | void _imbalance(int tiles) { |
| 43 | final rows = (_view.height / tiles).ceil(); |
| 44 | final times = <int>[]; |
| 45 | for (var t = 0; t < tiles; t++) { |
| 46 | final y0 = t * rows; |
| 47 | final y1 = ((t + 1) * rows).clamp(0, _view.height); |
| 48 | if (y0 >= y1) continue; |
| 49 | final sw = Stopwatch()..start(); |
| 50 | v.renderBand(_view, y0, y1); |
| 51 | sw.stop(); |
| 52 | times.add(sw.elapsedMicroseconds); |
| 53 | } |
| 54 | final total = times.reduce((a, b) => a + b); |
| 55 | final slowest = times.reduce((a, b) => a > b ? a : b); |
| 56 | final mean = total / times.length; |
| 57 | print(' $tiles bands: slowest ${(slowest / 1000).toStringAsFixed(1)} ms ' |
| 58 | 'vs mean ${(mean / 1000).toStringAsFixed(1)} ms ' |
| 59 | '(${(slowest / mean).toStringAsFixed(1)}x — the frame waits on this)'); |
| 60 | } |
| 61 | |
| 62 | int _best(void Function() f) { |
| 63 | var best = 1 << 30; |
| 64 | for (var i = 0; i < 3; i++) { |
| 65 | final sw = Stopwatch()..start(); |
| 66 | f(); |
| 67 | sw.stop(); |
| 68 | best = sw.elapsedMilliseconds < best ? sw.elapsedMilliseconds : best; |
| 69 | } |
| 70 | return best; |
| 71 | } |
| 72 | |
| 73 | Future<int> _bestAsync(Future<void> Function() f) async { |
| 74 | var best = 1 << 30; |
| 75 | for (var i = 0; i < 3; i++) { |
| 76 | final sw = Stopwatch()..start(); |
| 77 | await f(); |
| 78 | sw.stop(); |
| 79 | best = sw.elapsedMilliseconds < best ? sw.elapsedMilliseconds : best; |
| 80 | } |
| 81 | return best; |
| 82 | } |