carray 3.0.5 → 3.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +233 -0
  3. data/README.md +1 -1
  4. data/carray.gemspec +3 -2
  5. data/ext/ca_axis_dispatch.c +6 -0
  6. data/ext/ca_axis_group.c +1 -1
  7. data/ext/ca_categorical_iterator.c +7 -7
  8. data/ext/ca_kernel_iterator.c +116 -231
  9. data/ext/ca_kernel_iterator.h +410 -254
  10. data/ext/ca_obj_bincmp.c +9 -1
  11. data/ext/ca_obj_binop.c +10 -1
  12. data/ext/ca_obj_bitfield.c +2 -0
  13. data/ext/ca_obj_byte_swap.c +2 -0
  14. data/ext/ca_obj_face.c +2 -0
  15. data/ext/ca_obj_fake.c +3 -1
  16. data/ext/ca_obj_grid.c +167 -12
  17. data/ext/ca_obj_meld.c +69 -113
  18. data/ext/ca_obj_moncmp.c +9 -1
  19. data/ext/ca_obj_monop.c +40 -2
  20. data/ext/ca_obj_object.c +54 -20
  21. data/ext/ca_obj_reduce.c +15 -1
  22. data/ext/ca_obj_refer.c +65 -20
  23. data/ext/ca_obj_remap.c +69 -10
  24. data/ext/ca_obj_roll.c +1 -0
  25. data/ext/ca_obj_select.c +4 -1
  26. data/ext/ca_obj_select_axis.c +1 -0
  27. data/ext/ca_obj_shift.c +1 -0
  28. data/ext/ca_obj_stack.c +310 -111
  29. data/ext/ca_obj_stride.c +131 -45
  30. data/ext/ca_obj_string.c +18 -2
  31. data/ext/ca_obj_tile.c +1 -0
  32. data/ext/ca_obj_triop.c +9 -1
  33. data/ext/ca_obj_window.c +1 -0
  34. data/ext/ca_op_ipower.c +8 -8
  35. data/ext/caframe_csv_reader.c +1 -1
  36. data/ext/carray.h +2 -1
  37. data/ext/carray_access.c +27 -64
  38. data/ext/carray_address_basis.c +134 -0
  39. data/ext/carray_attribute.c +8 -0
  40. data/ext/carray_broadcast.c +3 -4
  41. data/ext/carray_build_flags.h +1 -1
  42. data/ext/carray_cast.c +8 -11
  43. data/ext/carray_conversion.c +9 -5
  44. data/ext/carray_core.c +811 -72
  45. data/ext/carray_element.c +13 -9
  46. data/ext/carray_histogram.c +25 -3
  47. data/ext/carray_internal.h +147 -12
  48. data/ext/carray_kernels_bincmp.c +2007 -3
  49. data/ext/carray_kernels_binop.c +25365 -5199
  50. data/ext/carray_kernels_init.c +8 -8
  51. data/ext/carray_kernels_monop.c +10696 -3678
  52. data/ext/carray_kernels_reduce_aggregate.c +4429 -702
  53. data/ext/carray_kernels_reduce_boolean.c +1533 -15
  54. data/ext/carray_kernels_reduce_cumulative.c +1808 -248
  55. data/ext/carray_kernels_reduce_extreme.c +4508 -511
  56. data/ext/carray_kernels_reduce_variance.c +2284 -152
  57. data/ext/carray_kernels_scan.c +240 -240
  58. data/ext/carray_kernels_search.c +4211 -264
  59. data/ext/carray_kernels_sort.c +367 -450
  60. data/ext/carray_lazy.c +47 -0
  61. data/ext/carray_loop.c +1 -1
  62. data/ext/carray_mask.c +130 -139
  63. data/ext/carray_median_percentile.c +4 -4
  64. data/ext/carray_memory_view.c +13 -7
  65. data/ext/carray_operator.c +147 -44
  66. data/ext/carray_order.c +3 -2
  67. data/ext/carray_random.c +4 -1
  68. data/ext/carray_slab.c +97 -21
  69. data/ext/carray_sort.c +1 -1
  70. data/ext/carray_test.c +74 -19
  71. data/ext/carray_time_text.c +2 -2
  72. data/ext/extconf.rb +11 -0
  73. data/ext/mkkernel.rb +346 -94
  74. data/ext/ruby_carray.c +4 -1
  75. data/ext/version.h +4 -4
  76. data/lib/carray/attribute.rb +25 -1
  77. data/lib/carray/autoload_carray.rb +3 -3
  78. data/lib/carray/autoload_method_extension.rb +5 -3
  79. data/lib/carray/axis_group.rb +13 -7
  80. data/lib/carray/basics.rb +1 -1
  81. data/lib/carray/block_iterator.rb +17 -3
  82. data/lib/carray/categorical.rb +6 -8
  83. data/lib/carray/categorical_iterator.rb +2 -2
  84. data/lib/carray/complex.rb +2 -2
  85. data/lib/carray/conditional.rb +3 -2
  86. data/lib/carray/frame/column_verbs.rb +45 -14
  87. data/lib/carray/frame/convert.rb +19 -0
  88. data/lib/carray/frame/csv_parser.rb +32 -32
  89. data/lib/carray/frame/describe.rb +4 -2
  90. data/lib/carray/frame/frame.rb +399 -78
  91. data/lib/carray/frame/group.rb +679 -53
  92. data/lib/carray/frame/io.rb +209 -71
  93. data/lib/carray/frame/join.rb +146 -37
  94. data/lib/carray/frame/records.rb +3 -3
  95. data/lib/carray/frame/reshape.rb +89 -11
  96. data/lib/carray/frame/sort.rb +57 -9
  97. data/lib/carray/frame/verbs.rb +384 -49
  98. data/lib/carray/frame/windows.rb +44 -6
  99. data/lib/carray/inspect.rb +7 -2
  100. data/lib/carray/iterator.rb +32 -0
  101. data/lib/carray/lazy.rb +17 -8
  102. data/lib/carray/mask_gap_fill.rb +37 -17
  103. data/lib/carray/meld_reduce.rb +1 -1
  104. data/lib/carray/methods/choose.rb +3 -2
  105. data/lib/carray/methods/meshgrid.rb +3 -1
  106. data/lib/carray/runtime.rb +47 -8
  107. data/lib/carray/segment_iterator.rb +23 -8
  108. data/lib/carray/serialize.rb +34 -17
  109. data/lib/carray/stack.rb +18 -6
  110. data/lib/carray/time.rb +5 -0
  111. data/lib/carray/window_iterator.rb +16 -4
  112. data/yard-stubs/ca_obj_array.rb +26 -1
  113. data/yard-stubs/carray_cast.rb +8 -5
  114. data/yard-stubs/carray_conversion.rb +8 -2
  115. data/yard-stubs/carray_mask.rb +20 -1
  116. data/yard-stubs/carray_math.rb +31 -16
  117. data/yard-stubs/carray_memory_view.rb +7 -0
  118. data/yard-stubs/carray_test.rb +7 -4
  119. metadata +18 -4
@@ -31,7 +31,8 @@
31
31
 
32
32
  A route added for speed is a shortcut, tagged next to its code with
33
33
  "Shortcut NAME" and a fixed set of fields; each one is switched off by
34
- ca_fast_paths_enabled().
34
+ ca_fast_path(CA_SC_name): by its name in CARRAY_SHORTCUTS_OFF, or with
35
+ the others by CARRAY_GENERAL_PATH=1 (carray_internal.h).
35
36
 
36
37
  ---------------------------------------------------------------------------- */
37
38
 
@@ -43,7 +44,6 @@
43
44
 
44
45
  #include <assert.h>
45
46
  #include <string.h>
46
- #include <ruby/ractor.h>
47
47
 
48
48
  /* Private flags bit, kept in ca_iter_state.flags like
49
49
  CA_ITER_FLAG_WALK_MAY_RAISE below: src is a copy the walk owns (see
@@ -52,69 +52,7 @@
52
52
 
53
53
  /* Data scratch. A scratch of object cells holds VALUEs no Ruby object
54
54
  owns while the kernel calls Ruby for each cell, so it is filled with
55
- Qnil and held for the GC until finish releases it.
56
-
57
- One large scratch is kept when a walk frees it and handed to the next
58
- walk that fits in it: a fresh allocation of that size costs the page
59
- faults of touching it, which for a reduction is several times the
60
- reduction itself. The kept buffer is owned by nobody, and a walk that
61
- takes it owns it alone until it frees it again. Each Ractor keeps its
62
- own (ractor-local storage), since walks in different Ractors run at
63
- the same time.
64
-
65
- Shortcut KEEP_SCRATCH (name tag; docs/internal/KernelIteratorEngine.md
66
- section 3.4 has the table of all shortcuts).
67
- Layer data: where a scratch comes from. The walk is unchanged.
68
- Speeds up walks with a scratch of 1 MiB or more, run one after
69
- another.
70
- Why no difference measured on macOS (a per-axis sum of a
71
- 2000x2000 fake view); the page faults it
72
- avoids are expected to cost more on Linux (unmeasured).
73
- Enters ki_data_alloc: bytes >= KI_KEEP_MIN and a kept buffer
74
- that fits; ki_data_free: KI_KEEP_MIN <= bytes <=
75
- KI_KEEP_MAX.
76
- Promises 4 (a kept buffer has no owner, so no exit can leave it
77
- held); 7 (object cells are cleared and held either way).
78
- Switch ca_fast_paths_enabled(): off, every scratch is fresh.
79
- Checked by test_iter_ractor_scratch.rb; the buffer count in
80
- test_iter_delivery_detectors.rb. */
81
-
82
- #define KI_KEEP_MIN ((ca_size_t) 1 << 20)
83
- #define KI_KEEP_MAX ((ca_size_t) 64 << 20)
84
-
85
- typedef struct {
86
- void *p;
87
- ca_size_t bytes;
88
- } ki_kept_t;
89
-
90
- static rb_ractor_local_key_t ki_kept_key;
91
-
92
- static void
93
- ki_kept_free (void *ptr)
94
- {
95
- ki_kept_t *k = (ki_kept_t *) ptr;
96
- if ( k ) {
97
- if ( k->p ) ruby_xfree(k->p);
98
- ruby_xfree(k);
99
- }
100
- }
101
-
102
- static const struct rb_ractor_local_storage_type ki_kept_type = {
103
- NULL, ki_kept_free
104
- };
105
-
106
- static ki_kept_t *
107
- ki_kept_slot (void)
108
- {
109
- ki_kept_t *k = (ki_kept_t *) rb_ractor_local_storage_ptr(ki_kept_key);
110
- if ( k == NULL ) {
111
- k = ALLOC(ki_kept_t);
112
- k->p = NULL;
113
- k->bytes = 0;
114
- rb_ractor_local_storage_ptr_set(ki_kept_key, k);
115
- }
116
- return k;
117
- }
55
+ Qnil and held for the GC until finish releases it. */
118
56
 
119
57
  /* Development build: the number of buffers ki_data_alloc has handed out
120
58
  that ki_data_free has not taken back (CArray.__iter_buffers__). A walk
@@ -131,19 +69,9 @@ static long ki_buffers_out = 0;
131
69
  static void *
132
70
  ki_data_alloc (int8_t data_type, ca_size_t bytes)
133
71
  {
134
- void *p = NULL;
72
+ void *p;
135
73
  KI_BUFFERS_OUT(1);
136
- if ( bytes >= KI_KEEP_MIN && ca_fast_paths_enabled() ) {
137
- ki_kept_t *k = ki_kept_slot();
138
- if ( k->p && bytes <= k->bytes ) {
139
- p = k->p;
140
- k->p = NULL;
141
- k->bytes = 0;
142
- }
143
- }
144
- if ( p == NULL ) {
145
- p = xmalloc(bytes > 0 ? bytes : 1);
146
- }
74
+ p = xmalloc(bytes > 0 ? bytes : 1);
147
75
  if ( data_type == CA_OBJECT ) {
148
76
  ca_size_t n = bytes / (ca_size_t) sizeof(VALUE);
149
77
  ca_object_cells_clear(p, n);
@@ -152,24 +80,11 @@ ki_data_alloc (int8_t data_type, ca_size_t bytes)
152
80
  return p;
153
81
  }
154
82
 
155
- /* `bytes` is the size p was allocated with (0 when unknown: never kept). */
156
83
  static void
157
- ki_data_free (void *p, ca_size_t bytes)
84
+ ki_data_free (void *p)
158
85
  {
159
86
  KI_BUFFERS_OUT(-1);
160
87
  ca_gc_release_buffer(p);
161
- if ( bytes >= KI_KEEP_MIN && bytes <= KI_KEEP_MAX && ca_fast_paths_enabled() ) {
162
- ki_kept_t *k = ki_kept_slot();
163
- if ( bytes > k->bytes ) {
164
- void *old = k->p;
165
- k->p = p;
166
- k->bytes = bytes;
167
- if ( old ) {
168
- xfree(old);
169
- }
170
- return;
171
- }
172
- }
173
88
  xfree(p);
174
89
  }
175
90
 
@@ -197,7 +112,7 @@ extern ca_operation_function_t ca_window_func;
197
112
  engine gathers its cells into a scratch buffer of its own through the
198
113
  view's transfer (ki_gather_view = ca_xfer_all GET) and, on a WRITE
199
114
  walk, sends back the cells the kernel changed (ki_scratch_push_changed
200
- = ca_xfer_addrs, or ca_xfer_all PUT when every cell changed). The
115
+ = ca_xfer_addrs). The
201
116
  view's attach / sync slots are not called. Cast / byte swap / bit
202
117
  unpack / reduction views: */
203
118
  extern ca_operation_function_t ca_fake_func;
@@ -272,6 +187,14 @@ ca_iter_register_source_kind (int obj_type, uint8_t kind)
272
187
  "ca_iter_register_source_kind: obj_type %d out of range",
273
188
  obj_type);
274
189
  }
190
+ /* carray's own types are classified from their operation tables; a
191
+ registration would only move one of them onto the transfer walk, for
192
+ every array in the process. */
193
+ if ( obj_type < ca_obj_num_builtin ) {
194
+ rb_raise(rb_eArgError,
195
+ "ca_iter_register_source_kind: obj_type %d is one of carray's "
196
+ "own", obj_type);
197
+ }
275
198
  if ( kind != CA_ITER_SRC_ATTACH ) {
276
199
  rb_raise(rb_eArgError,
277
200
  "ca_iter_register_source_kind: only CA_ITER_SRC_ATTACH (%d) "
@@ -497,10 +420,10 @@ ca_iter_build_rowmajor_strides (ca_size_t *strides,
497
420
  /* ---- PER_FIBER_FUSED: which views take it ----------------------------- */
498
421
 
499
422
  /* A src_kind branch, then a check of the view's operation table, then of
500
- the fiber axis's stride. Returns nonzero for
423
+ the fiber axis's stride. Returns nonzero for a read of
501
424
  CAFake / CAByteSwap / a single-cast CAMonOp under SRC_ATTACH when the
502
- fiber is contiguous in the parent, and for CAWindow / CAShift under
503
- SRC_DESCRIPTOR; zero otherwise.
425
+ fiber is contiguous in the parent (that is, the fiber runs along the
426
+ innermost axis); zero for a write walk and for every other view.
504
427
 
505
428
  The fiber_axis_stride argument is the effective byte stride of the
506
429
  fiber axis on the view (= what next_slab_axes would yield as
@@ -508,10 +431,10 @@ ca_iter_build_rowmajor_strides (ca_size_t *strides,
508
431
  a contiguous run of the parent's memory, which is what lets the view's
509
432
  region transfer deliver it in one pass.
510
433
 
511
- Called from two places in init_l2: the CA_SLAB_AXES descriptor branch
512
- and the SRC_ATTACH branch, each before it would materialise. When it
513
- returns nonzero the caller skips the materialise and sets alias_mode =
514
- CA_ITER_ALIAS_PER_FIBER_FUSED with the fiber dispatch state filled. */
434
+ Called from one place, the SRC_ATTACH branch of ki_choose_l2_route,
435
+ with src_kind = CA_ITER_SRC_ATTACH. When it returns nonzero the route
436
+ is KI_L2_FIBER_FUSED: the whole view is not gathered and alias_mode is
437
+ CA_ITER_ALIAS_PER_FIBER_FUSED. */
515
438
  static int
516
439
  ca_iter_should_per_fiber_fused (CArray *src,
517
440
  int src_kind,
@@ -543,19 +466,6 @@ ca_iter_should_per_fiber_fused (CArray *src,
543
466
  }
544
467
  }
545
468
 
546
- /* CAShift / CAWindow: the view's region transfer fills out-of-bounds
547
- cells itself, so a fiber can be fetched in one call instead of a
548
- per-slab or whole-view gather (SRC_DESCRIPTOR). A view that can
549
- lend the parent's memory (SRC_DESCRIPTOR_L2_ALIASABLE, an interior
550
- CAWindow) is already read in place; fetching it fiber by fiber was
551
- measured slower (0.35-0.82x), so only SRC_DESCRIPTOR takes it. */
552
- if ( src_kind == CA_ITER_SRC_DESCRIPTOR ) {
553
- /* CAWindow and CAShift share ca_window_func.attach. */
554
- if ( attach == ca_window_func.attach ) {
555
- return 1;
556
- }
557
- }
558
-
559
469
  /* Not taken by the other views, each measured slower fiber by fiber:
560
470
  CASelectAxis / CAGrid / CASelect (1.25-4.7x), whose descriptor
561
471
  engine already gathers the whole view in one batched pass and has no
@@ -645,9 +555,9 @@ ca_iter_strip_storage_wrapper (CArray *src)
645
555
  return src;
646
556
  }
647
557
 
648
- /* Raise unless a and b have the same shape. The INOUT and PAIR fiber
649
- macros call it before opening either walk, so a refusal leaves nothing
650
- to close. */
558
+ /* Raise unless a and b have the same shape. The INOUT, SLAB_INOUT and
559
+ PAIR macros call it before opening either walk, so a refusal leaves
560
+ nothing to close. */
651
561
  int
652
562
  ca_iter_check_same_shape (struct _CArray *a, struct _CArray *b)
653
563
  {
@@ -817,19 +727,7 @@ ki_scratch_push_changed (ca_iter_state *st)
817
727
  }
818
728
  }
819
729
 
820
- /* Shortcut PUSH_ALL_CHANGED.
821
- Layer write-back.
822
- Speeds up a whole-view write walk that changed every cell: one
823
- ca_xfer_all PUT instead of a list of all the addresses.
824
- Why about 5% on a whole write of a fake view of 1M cells.
825
- Enters whole && n_changed == n.
826
- Promises 3 (the same cells go back; the list would hold them all).
827
- Switch ca_fast_paths_enabled(): off, the address list.
828
- Checked by the write rows of the delivery matrix (L1 / L2 whole). */
829
- if ( whole && n_changed == n && ca_fast_paths_enabled() ) {
830
- ca_xfer_all(src, now, CA_XFER_PUT);
831
- }
832
- else if ( n_changed > 0 ) {
730
+ if ( n_changed > 0 ) {
833
731
  ca_xfer_addrs(src, n_changed, addrs, vals, CA_XFER_PUT);
834
732
  }
835
733
  ALLOCV_END(h_vals);
@@ -1027,8 +925,8 @@ ca_iter_state_init_l1_unprotected (ca_iter_state *st,
1027
925
  state is assembled in one place and init_l2 has one way out. */
1028
926
 
1029
927
  typedef enum {
1030
- KI_L2_FIBER_FUSED, /* PER_FIBER_FUSED: a CAWindow / CAShift fiber, or a
1031
- read of a conversion view's innermost fiber */
928
+ KI_L2_FIBER_FUSED, /* PER_FIBER_FUSED: a read of a conversion view's
929
+ innermost fiber */
1032
930
  KI_L2_PER_SLAB, /* descriptor view, read, a slab axis INDEX or SHIFT:
1033
931
  one slab gathered at a time (PER_SLAB / _HOIST) */
1034
932
  KI_L2_DESC_WHOLE, /* descriptor view, read: the whole view gathered by
@@ -1135,33 +1033,6 @@ ki_choose_l2_route (CArray *src, int8_t *axes, int8_t naxes, uint32_t flags,
1135
1033
 
1136
1034
  if ( plan->src_kind == CA_ITER_SRC_DESCRIPTOR
1137
1035
  || plan->src_kind == CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE ) {
1138
- /* Shortcut PER_FIBER_FUSED (CAWindow / CAShift).
1139
- Layer all three, per fiber: each fiber is fetched with the
1140
- view's ca_xfer_stride (which fills out-of-bounds
1141
- cells itself) and put back the same way.
1142
- Speeds up a fiber walk of a CAWindow / CAShift whose innermost
1143
- axis is SHIFT.
1144
- Why none measured: about 8% slower than the descriptor
1145
- route on a sort of a 2000x2000 shift view.
1146
- Enters CA_KERNEL_FIBER_CONTIG, one slab axis, the view's
1147
- own rank, and ca_iter_should_per_fiber_fused. An
1148
- outer-only SHIFT arrives as L2_ALIASABLE and never
1149
- reaches this hook.
1150
- Promises 1 (the fiber scratch is contiguous), 2 (the mask is
1151
- fetched into the fiber mask scratch), 3 (ca_xfer_stride
1152
- PUT of a fiber that neither converts nor repeats a
1153
- cell), 4 (CA_ITER_FLAG_WALK_MAY_RAISE).
1154
- Switch ca_fast_paths_enabled(): off, the descriptor route.
1155
- Checked by the fiber rows of the "shift" view in the delivery
1156
- matrix. */
1157
- if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1
1158
- && plan->ndesc == src->ndim && ca_fast_paths_enabled()
1159
- && ca_iter_should_per_fiber_fused(src, plan->src_kind, axes[0],
1160
- row_bytes[axes[0]], flags) ) {
1161
- plan->route = KI_L2_FIBER_FUSED;
1162
- return CA_ITER_OK;
1163
- }
1164
-
1165
1036
  /* The rank check guards a descriptor whose rank differs from the
1166
1037
  view's (it would not expose the axis partition slab_axes names).
1167
1038
  Every describe_axes in the engine reports the view's rank, so the
@@ -1190,19 +1061,21 @@ ki_choose_l2_route (CArray *src, int8_t *axes, int8_t naxes, uint32_t flags,
1190
1061
  Layer data; the fiber layer (ki_fiber_contig_*) sits on top.
1191
1062
  Speeds up read walks of a CAWindow / CAShift whose slab has a
1192
1063
  SHIFT axis and not the innermost axis.
1193
- Why about 1.5x on a median along axis 0 of a 2000x2000
1194
- shift view.
1064
+ Why measured switched off alone (CARRAY_SHORTCUTS_OFF) on an
1065
+ M2 Max / a Xeon 6369P, 2026-10-26: 1.5x / 1.1x on a
1066
+ median along axis 0 of a 2000x2000 shift view, 3.1x /
1067
+ 2.2x on a sum along axis 0 of a window.
1195
1068
  Enters a non-stride slab axis, no INDEX slab axis, the
1196
1069
  innermost axis outside the slab, a read walk.
1197
1070
  Promises 2 and 6 (the mask is copied by ki_take_mask_copy, and
1198
1071
  only when the view has one).
1199
- Switch ca_fast_paths_enabled(): off, the per-slab gather.
1072
+ Switch ca_fast_path(): off, the per-slab gather.
1200
1073
  Checked by the slab-axis rows of the "shift" and "window" views
1201
1074
  in the delivery matrix. */
1202
1075
  int whole_view = slab_has_non_stride && ! slab_has_index
1203
1076
  && ! in_slab[src->ndim - 1]
1204
1077
  && ! ( flags & CA_KERNEL_WRITE )
1205
- && ca_fast_paths_enabled();
1078
+ && ca_fast_path(CA_SC_SHIFT_WHOLE_VIEW);
1206
1079
 
1207
1080
  /* The descriptor engine's gathers (per slab and whole view) read
1208
1081
  only. A write takes the transfer walk instead: the view is
@@ -1234,19 +1107,20 @@ ki_choose_l2_route (CArray *src, int8_t *axes, int8_t naxes, uint32_t flags,
1234
1107
  parents through a tile cache.
1235
1108
  Speeds up read walks of an unmasked CAStack whose one slab axis
1236
1109
  is the stack axis.
1237
- Why about 2x on a mean along the stack axis of 4 parents
1238
- of 500x2000.
1110
+ Why measured switched off alone (CARRAY_SHORTCUTS_OFF) on an
1111
+ M2 Max / a Xeon 6369P, 2026-10-26: 1.2x / 1.3x on a mean
1112
+ along the stack axis of 4 parents of 500x2000.
1239
1113
  Enters a CAStack, no mask, a read walk, one slab axis that is
1240
1114
  k_axis.
1241
1115
  Promises 1 (the slab is contiguous), 6 (no mask exists to touch).
1242
- Switch ca_fast_paths_enabled(): off, the transfer walk.
1116
+ Switch ca_fast_path(): off, the transfer walk.
1243
1117
  Checked by the slab-axis and fiber rows of the stack parent along
1244
1118
  axis 0 in the delivery matrix. */
1245
1119
  if ( ca_func[src->obj_type].attach == ca_stack_func.attach
1246
1120
  && !ca_has_mask(src)
1247
1121
  && !(flags & CA_KERNEL_WRITE)
1248
1122
  && naxes == 1 && axes[0] == ((CAStack *) src)->k_axis
1249
- && ca_fast_paths_enabled() ) {
1123
+ && ca_fast_path(CA_SC_STACK) ) {
1250
1124
  plan->route = KI_L2_STACK;
1251
1125
  return CA_ITER_OK;
1252
1126
  }
@@ -1255,19 +1129,22 @@ ki_choose_l2_route (CArray *src, int8_t *axes, int8_t naxes, uint32_t flags,
1255
1129
  Speeds up read walks of a CAStack whose stack axis is an outer
1256
1130
  axis: each slab is cut from the one parent the outer
1257
1131
  index picks, without gathering the stack.
1258
- Why about 11x on a mean along axis 1 of 4 parents of
1259
- 500x2000.
1132
+ Why measured switched off alone (CARRAY_SHORTCUTS_OFF) on an
1133
+ M2 Max / a Xeon 6369P, 2026-10-26: 1.0x / 1.0x on a mean
1134
+ along axis 1 of 4 parents of 500x2000, which the
1135
+ generated loop interchange answers first (3.5x / 3.8x
1136
+ with that off too); 1.1x / 1.2x on a median along axis 2.
1260
1137
  Enters a CAStack, a read walk, the stack axis outside the slab.
1261
1138
  Promises 2 (the parents' masks are copied), 6 (masks are read
1262
1139
  only when a parent has one), 8 (alias_ptr stays NULL,
1263
1140
  so no slab is lent as a window).
1264
- Switch ca_fast_paths_enabled(): off, the transfer walk.
1141
+ Switch ca_fast_path(): off, the transfer walk.
1265
1142
  Checked by the slab-axis, fiber and two-slab-axes rows of the
1266
1143
  stack parent along axes 1 and 2 in the delivery matrix. */
1267
1144
  if ( ca_func[src->obj_type].attach == ca_stack_func.attach
1268
1145
  && !(flags & CA_KERNEL_WRITE)
1269
1146
  && naxes >= 1 && !in_slab[((CAStack *) src)->k_axis]
1270
- && ca_fast_paths_enabled() ) {
1147
+ && ca_fast_path(CA_SC_STACK_OUTER_K) ) {
1271
1148
  plan->route = KI_L2_STACK_OUTER_K;
1272
1149
  return CA_ITER_OK;
1273
1150
  }
@@ -1277,17 +1154,20 @@ ki_choose_l2_route (CArray *src, int8_t *axes, int8_t naxes, uint32_t flags,
1277
1154
  gathering the whole view.
1278
1155
  Speeds up a read of a fiber of a fake or byte-swap view, or of a
1279
1156
  lazy monop that only casts, along its innermost axis.
1280
- Why no difference measured on a sort along the last axis
1281
- of a 2000x2000 fake view.
1157
+ Why measured switched off alone (CARRAY_SHORTCUTS_OFF) on an
1158
+ M2 Max / a Xeon 6369P, 2026-10-26: 1.0x / 1.1x on a sort
1159
+ and 1.05x / 1.2x on a median along the last axis of a
1160
+ 2000x2000 fake view.
1282
1161
  Enters CA_KERNEL_FIBER_CONTIG, one slab axis, a read walk, and
1283
1162
  ca_iter_should_per_fiber_fused.
1284
- Promises 1, 2 and 4, as the CAWindow / CAShift hook above. A
1285
- write takes the transfer walk (promise 3).
1286
- Switch ca_fast_paths_enabled(): off, the transfer walk.
1163
+ Promises 1 (the fiber is fetched packed), 2 (its mask is fetched
1164
+ the same way into fiber_mask_scratch) and 4. A write
1165
+ takes the transfer walk (promise 3).
1166
+ Switch ca_fast_path(): off, the transfer walk.
1287
1167
  Checked by the fiber rows of the fake parent in the delivery
1288
1168
  matrix. */
1289
1169
  if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1
1290
- && ca_fast_paths_enabled()
1170
+ && ca_fast_path(CA_SC_FIBER_FUSED_CONVERSION)
1291
1171
  && ca_iter_should_per_fiber_fused(src, CA_ITER_SRC_ATTACH, axes[0],
1292
1172
  row_bytes[axes[0]], flags) ) {
1293
1173
  plan->route = KI_L2_FIBER_FUSED;
@@ -1588,16 +1468,19 @@ ki_open_l2 (ca_iter_state *st, CArray *src, ca_slab_policy_t policy,
1588
1468
  and with no SHIFT axis in the view: the outer and
1589
1469
  non-innermost slab offsets are hoisted and the inner
1590
1470
  run is copied with memcpy.
1591
- Why no difference measured on a row-selected 2-D read.
1471
+ Why measured switched off alone (CARRAY_SHORTCUTS_OFF) on an
1472
+ M2 Max, 2026-10-26: 1.6x on a mean over two slab axes of
1473
+ a 400x200x200 array with rows selected. Not taken with
1474
+ one slab axis.
1592
1475
  Enters at least one slab axis, the innermost slab axis
1593
1476
  STRIDE, no SHIFT axis.
1594
1477
  Promises 2 and 6 (the mask is copied by ki_take_mask_copy, and
1595
1478
  only when the view has one).
1596
- Switch ca_fast_paths_enabled(): off, the PER_SLAB gather.
1479
+ Switch ca_fast_path(): off, the PER_SLAB gather.
1597
1480
  Checked by the two-slab-axes rows of the grid parent in the
1598
1481
  delivery matrix. */
1599
1482
  ca_size_t slab_bytes = st->slab_elements * src->bytes;
1600
- int hoist = (st->slab_ndim > 0) && ca_fast_paths_enabled()
1483
+ int hoist = (st->slab_ndim > 0) && ca_fast_path(CA_SC_PER_SLAB_HOIST)
1601
1484
  && plan->descs[st->slab_axes_buf[st->slab_ndim - 1]].kind
1602
1485
  == CA_AXIS_KIND_STRIDE;
1603
1486
  for ( k = 0; hoist && k < src->ndim; k++ ) {
@@ -1745,7 +1628,7 @@ ki_fiber_contig_data (ca_iter_state *st, char *data, ca_size_t step)
1745
1628
  return data;
1746
1629
  }
1747
1630
  if ( st->fiber_data_scratch_cap < need ) {
1748
- if ( st->fiber_data_scratch ) ki_data_free(st->fiber_data_scratch, st->fiber_data_scratch_cap);
1631
+ if ( st->fiber_data_scratch ) ki_data_free(st->fiber_data_scratch);
1749
1632
  st->fiber_data_scratch = (char *) ki_data_alloc(st->src->data_type, need);
1750
1633
  st->fiber_data_scratch_cap = need;
1751
1634
  }
@@ -2108,15 +1991,9 @@ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2108
1991
  /* PER_FIBER_FUSED: there is no whole-view buffer to alias from. Build
2109
1992
  the fiber region from outer_idx + fiber_axis and call
2110
1993
  ca_xfer_stride(src, ..., GET) directly into fiber_data_scratch; the
2111
- view's region transfer (out-of-bounds filling for CAWindow / CAShift,
2112
- the conversion for a fake or byte-swap view) delivers it in one
2113
- pass.
2114
-
2115
- last_data_off carries the fiber region's outer footprint so
2116
- sync_slab can reconstruct the same region for WRITE PUT. Encode
2117
- it as the linear outer_idx position scaled by fiber_axis stride
2118
- equivalence; for PER_FIBER_FUSED sync_slab reads outer_idx state
2119
- directly rather than data_off so the value is informational. */
1994
+ view's region transfer (the conversion for a fake, byte-swap or
1995
+ single-cast lazy view) delivers it in one pass. The route is read
1996
+ only, so nothing is put back; last_data_off is not used. */
2120
1997
  if ( st->alias_mode == CA_ITER_ALIAS_PER_FIBER_FUSED ) {
2121
1998
  ca_size_t fiber_n = st->slab_dims[0];
2122
1999
  ca_size_t need = fiber_n * st->bytes;
@@ -2126,16 +2003,13 @@ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2126
2003
  ca_size_t counts[CA_RANK_MAX];
2127
2004
 
2128
2005
  if ( st->fiber_data_scratch_cap < need ) {
2129
- if ( st->fiber_data_scratch ) ki_data_free(st->fiber_data_scratch, st->fiber_data_scratch_cap);
2006
+ if ( st->fiber_data_scratch ) ki_data_free(st->fiber_data_scratch);
2130
2007
  st->fiber_data_scratch = (char *) ki_data_alloc(st->src->data_type, need);
2131
2008
  st->fiber_data_scratch_cap = need;
2132
2009
  }
2133
2010
 
2134
2011
  /* Build fiber region: fiber_axis spans the full fiber, all other
2135
- axes pinned to outer_idx position (count=1). Cache starts[] in
2136
- state so sync_slab can rebuild the same region for WRITE PUT
2137
- (= captured BEFORE the outer_idx advance below, the same hazard as
2138
- last_data_off; see sync_slab). */
2012
+ axes pinned to outer_idx position (count=1). */
2139
2013
  {
2140
2014
  int8_t op = 0;
2141
2015
  for ( int8_t k = 0; k < nd; k++ ) {
@@ -2147,7 +2021,6 @@ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2147
2021
  counts[k] = 1;
2148
2022
  op++;
2149
2023
  }
2150
- st->fiber_region_starts[k] = starts[k];
2151
2024
  }
2152
2025
  }
2153
2026
 
@@ -2276,27 +2149,6 @@ ca_iter_state_sync_slab_unprotected (ca_iter_state *st)
2276
2149
  * instead of reusing -- separate phase, separate design.
2277
2150
  * ====================================================================== */
2278
2151
 
2279
- /* PER_FIBER_FUSED write: the author
2280
- wrote into fiber_data_scratch and there is no whole-view buffer
2281
- to xfer_all PUT. Rebuild the same fiber region from cached
2282
- fiber_region_starts[] (= captured pre-advance in next_slab_axes,
2283
- the same hazard as last_data_off) and call ca_xfer_stride(src, ...,
2284
- PUT), which puts the fiber back through the view's region transfer
2285
- in one pass. */
2286
- if ( st->alias_mode == CA_ITER_ALIAS_PER_FIBER_FUSED ) {
2287
- ca_size_t fiber_n = st->slab_dims[0];
2288
- int8_t nd = st->ndim;
2289
- ca_size_t counts[CA_RANK_MAX];
2290
- for ( int8_t k = 0; k < nd; k++ ) {
2291
- counts[k] = (k == st->fiber_axis) ? fiber_n : 1;
2292
- }
2293
- ca_xfer_stride(st->src, st->fiber_region_starts, counts,
2294
- st->fiber_native_strides,
2295
- st->fiber_data_scratch, CA_XFER_PUT);
2296
- st->write_dirty = 0;
2297
- return;
2298
- }
2299
-
2300
2152
  /* Per-fiber gather path scatter.
2301
2153
  When next_slab_axes gathered the fiber into fiber_data_scratch
2302
2154
  (= naxes==1 + slab_strides[0] != bytes), scatter it back to the
@@ -2358,9 +2210,9 @@ ca_iter_state_sync_slab_unprotected (ca_iter_state *st)
2358
2210
  static VALUE ca_iter_finish_sync (VALUE arg);
2359
2211
 
2360
2212
  static VALUE
2361
- ki_sync_detach_root (VALUE root)
2213
+ ki_sync_detach (VALUE ca)
2362
2214
  {
2363
- ca_sync_detach((CArray *) root);
2215
+ ca_sync_detach((CArray *) ca);
2364
2216
  return Qnil;
2365
2217
  }
2366
2218
 
@@ -2389,7 +2241,7 @@ ca_iter_state_finish (ca_iter_state *st)
2389
2241
  at init. ca_detach, or ca_sync_detach on a WRITE walk.
2390
2242
  - else (alias paths): src was attached at init, detach. */
2391
2243
  if ( st->scratch_ptr ) {
2392
- ki_data_free(st->scratch_ptr, st->scratch_cap);
2244
+ ki_data_free(st->scratch_ptr);
2393
2245
  st->scratch_ptr = NULL;
2394
2246
  st->scratch_cap = 0;
2395
2247
  }
@@ -2399,7 +2251,7 @@ ca_iter_state_finish (ca_iter_state *st)
2399
2251
  }
2400
2252
  /* The per-fiber scratch. */
2401
2253
  if ( st->fiber_data_scratch ) {
2402
- ki_data_free(st->fiber_data_scratch, st->fiber_data_scratch_cap);
2254
+ ki_data_free(st->fiber_data_scratch);
2403
2255
  st->fiber_data_scratch = NULL;
2404
2256
  st->fiber_data_scratch_cap = 0;
2405
2257
  }
@@ -2422,7 +2274,7 @@ ca_iter_state_finish (ca_iter_state *st)
2422
2274
  }
2423
2275
  /* The STACK tile cache. */
2424
2276
  if ( st->stack_tile_cache ) {
2425
- ki_data_free(st->stack_tile_cache, 0);
2277
+ ki_data_free(st->stack_tile_cache);
2426
2278
  st->stack_tile_cache = NULL;
2427
2279
  st->stack_tile_cap = 0;
2428
2280
  st->stack_tile_pos = 0;
@@ -2461,7 +2313,7 @@ ca_iter_state_finish (ca_iter_state *st)
2461
2313
  block, a view lending no memory) gave a buffer of its own. */
2462
2314
  if ( write ) {
2463
2315
  /* protect: frees the iterator's own copy before the raise passes on */
2464
- rb_protect(ki_sync_detach_root, (VALUE) root, &tag);
2316
+ rb_protect(ki_sync_detach, (VALUE) root, &tag);
2465
2317
  }
2466
2318
  else {
2467
2319
  ca_detach(root);
@@ -2471,8 +2323,15 @@ ca_iter_state_finish (ca_iter_state *st)
2471
2323
  /* alias path (CONTIG/STRIDED for CAStride family): we attached
2472
2324
  src directly at init, so detach it here. A SRC_ATTACH walk owns
2473
2325
  its own scratch (alias_mode = NONE, freed above) and attached
2474
- nothing to detach. */
2475
- ca_detach(src);
2326
+ nothing to detach. A write walk syncs first: an entity whose
2327
+ attach hands out a copy (a cold one such as a CAObject mask)
2328
+ keeps what was written only through its sync slot. */
2329
+ if ( write ) {
2330
+ rb_protect(ki_sync_detach, (VALUE) src, &tag);
2331
+ }
2332
+ else {
2333
+ ca_detach(src);
2334
+ }
2476
2335
  }
2477
2336
  if ( own ) {
2478
2337
  if ( ca_is_object_type(own) ) {
@@ -2554,18 +2413,20 @@ typedef struct {
2554
2413
  Layer the exits (promise 4): which walks run next_slab /
2555
2414
  sync_slab under rb_protect.
2556
2415
  Speeds up walks that cannot raise there: they skip rb_protect.
2557
- Why about 1.5x on a per-axis sum over 200k short fibers.
2416
+ Why measured switched off alone (CARRAY_SHORTCUTS_OFF) on an
2417
+ M2 Max / a Xeon 6369P, 2026-10-26: 1.55x / 1.4x on a
2418
+ per-axis sum over 200k short fibers.
2558
2419
  Enters the route is not PER_FIBER_FUSED and the walk does not
2559
2420
  write back out of scratch.
2560
2421
  Promises 4 (a walk left out cannot raise between init and finish).
2561
- Switch ca_fast_paths_enabled(): off, every walk is protected.
2422
+ Switch ca_fast_path(): off, every walk is protected.
2562
2423
  Checked by test_iter_raise_cleanup.rb; the raise rows of
2563
2424
  test_iter_delivery_detectors.rb. */
2564
2425
  static void
2565
2426
  ca_iter_mark_walk_may_raise (ca_iter_state *st)
2566
2427
  {
2567
2428
  if ( st == NULL ) return;
2568
- if ( ! ca_fast_paths_enabled() ) {
2429
+ if ( ! ca_fast_path(CA_SC_PROTECT_ONLY_MAY_RAISE) ) {
2569
2430
  /* the general path protects every walk */
2570
2431
  st->flags |= CA_ITER_FLAG_WALK_MAY_RAISE;
2571
2432
  return;
@@ -2578,8 +2439,8 @@ ca_iter_mark_walk_may_raise (ca_iter_state *st)
2578
2439
  && (st->scratch_ptr != NULL
2579
2440
  || st->src_kind == CA_ITER_SRC_ATTACH) ) {
2580
2441
  /* writes back out of iterator-owned scratch, through the source's
2581
- transfer (ki_scratch_push_changed: ca_xfer_addrs / ca_xfer_all PUT)
2582
- or the descriptor scatter. A write into aliased memory needs no
2442
+ transfer (ki_scratch_push_changed: the changed cells by
2443
+ ca_xfer_addrs PUT). A write into aliased memory needs no
2583
2444
  push-back at all: sync_slab returns at once. */
2584
2445
  st->flags |= CA_ITER_FLAG_WALK_MAY_RAISE;
2585
2446
  }
@@ -2919,6 +2780,32 @@ ca_iter_ensure (int32_t n, ca_iter_state *states,
2919
2780
  return rb_ensure(body, arg, ca_iter_ensure_close, (VALUE) &e);
2920
2781
  }
2921
2782
 
2783
+ /* ca_iter_ensure over the object array ca, recorded as being computed
2784
+ over (ca_computing_run) for the length of the body. The record is taken
2785
+ inside the ensure, so a refusal closes the states as a raise from the
2786
+ body does. */
2787
+ typedef struct {
2788
+ CArray *ca;
2789
+ VALUE (*body)(VALUE);
2790
+ VALUE arg;
2791
+ } ca_iter_computing_t;
2792
+
2793
+ static VALUE
2794
+ ca_iter_computing_body (VALUE arg)
2795
+ {
2796
+ ca_iter_computing_t *c = (ca_iter_computing_t *) arg;
2797
+ return ca_computing_run(1, &c->ca, c->body, c->arg);
2798
+ }
2799
+
2800
+ VALUE
2801
+ ca_iter_ensure_computing (CArray *ca, int32_t n, ca_iter_state *states,
2802
+ VALUE (*body)(VALUE), VALUE arg)
2803
+ {
2804
+ ca_iter_computing_t c;
2805
+ c.ca = ca; c.body = body; c.arg = arg;
2806
+ return ca_iter_ensure(n, states, ca_iter_computing_body, (VALUE) &c);
2807
+ }
2808
+
2922
2809
  #ifdef CARRAY_DEV_BUILD
2923
2810
  /* ============================================================
2924
2811
  * smoke surface (dev-only, stripped in release)
@@ -3729,7 +3616,7 @@ rb_t1_smoke_remap_read_strided (VALUE klass, VALUE vref, VALUE vidx)
3729
3616
 
3730
3617
  /* WRITE smoke: fill view's slab buffer with `val` (float64), let
3731
3618
  sync_slab send the changed cells back to ref through the view
3732
- (ki_scratch_push_changed: ca_xfer_addrs or ca_xfer_all PUT). */
3619
+ (ki_scratch_push_changed: ca_xfer_addrs PUT). */
3733
3620
  static VALUE
3734
3621
  rb_t1_smoke_remap_write_fill_f64 (VALUE klass, VALUE vref, VALUE vidx,
3735
3622
  VALUE vval)
@@ -4223,8 +4110,6 @@ rb_ki_buffers_out (VALUE klass)
4223
4110
  void
4224
4111
  Init_ca_kernel_iterator (void)
4225
4112
  {
4226
- ki_kept_key = rb_ractor_local_storage_ptr_newkey(&ki_kept_type);
4227
-
4228
4113
  #ifdef CARRAY_DEV_BUILD
4229
4114
  /* ==== smoke surface registrations (dev-only, stripped in release) ====
4230
4115
  * All `t1_smoke_*`, `caf_smoke_*`,