carray-jit 0.1.1 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89d3c3c1c950f938ca30658e4d6586bd305b60e6d83083a33e645ea69168d3e3
4
- data.tar.gz: 6101247a5b517a6f28ce051016ced3674ce26e4400952a3a69eaf2ab8ea7eb40
3
+ metadata.gz: 988f9266a5aab773463b6e24ff6a9316993d2a820d17876f34bb048809066437
4
+ data.tar.gz: 4cf44dafad927c4d4b20e02aa6f057bebc6681b360fdfba396cc9c620f637fdb
5
5
  SHA512:
6
- metadata.gz: 6789499fd3fa7bbb2e2342bb48b35cb2eb6c5dbe05a2ef281cc4c9c734c6ef18c37ece9a929623103e5f1c388b656184bcc76e4b457b9c03241ae226c132e49c
7
- data.tar.gz: 16b2611b901be3ab35fd088309ba4976ac45f3cefd360a472971c95e7f0edcf17e3ebb19abb0bded712012b9b8e3c85cc26aea6787b9caf71741bc7b06b861f5
6
+ metadata.gz: 693790997ddab3484d377113866e6059bf29869ecc1921989ba1aaba76a33ea88f33b97ca7876832707d585e27061df37582c8710303e0a23a5cf56a4dbaaf30
7
+ data.tar.gz: 620704ee02ec5c638c185f8c416d98a8d45955634f2953cdef5f08613c6112faaba5b4c87a4b2bf7b928f4223370e75ba096c7fa7368939a3f4a1e0769c15387
data/CHANGELOG.md CHANGED
@@ -37,6 +37,92 @@ version you have and a newer one.
37
37
  a release needs is said in the gemspec, and an entry says so only
38
38
  when the answer changes. -->
39
39
 
40
+ ## 0.1.2
41
+
42
+ - New: `CArray.jit_contract` takes the result's axes as symbols, which says
43
+ which indices are free: `CArray.jit_contract(:p) { |k| x[p,k] * y[p,k] }` is
44
+ one number per point, and `CArray.jit_contract(:a) { q[a,a] }` is the
45
+ diagonal rather than the trace. A named index stays free however often it
46
+ appears, which is what an index that numbers things -- a point, a sample, a
47
+ batch -- does. What a repetition means is unchanged, so the whole rule is
48
+ that an index which repeats is summed and one that is named is free. Naming
49
+ the axes also states their order. With no arguments nothing changes.
50
+
51
+ - New: `CArray::JIT.contract_terms` runs the contraction a structure describes
52
+ rather than one a block writes --
53
+ `CArray::JIT.contract_terms([[a, [:i, :k]], [b, [:k, :j]]], free: [:i, :j])`
54
+ is a matrix product -- and `CArray::JIT.contraction_of` reads a block and
55
+ returns the terms it is a product of, or nil when it is not one. They are
56
+ `jit_contract` with the block taken out of the middle, for a caller that
57
+ rearranges a contraction before running it: the terms are compiled by the
58
+ same analyzer under the same rules, so `free:` is required and an index that
59
+ is not named must appear at more than one position.
60
+
61
+ - New: `CArray::JIT.cache_root = "path"` puts an application's compiled
62
+ kernels somewhere of its own, rather than in the cache shared under the home
63
+ directory. Say it before the first kernel is compiled; the path is expanded
64
+ where it is given. `CARRAY_JIT_CACHE` and `CARRAY_JIT_NO_CACHE` still come
65
+ first, and `nil` restores the default.
66
+
67
+ - Change: a contraction's sum is split into partial sums, as `jit_for`'s
68
+ reduction and CArray's own reduce kernels are, which makes `jit_contract` as
69
+ fast as the same loop written with `jit_for` rather than three times slower.
70
+ A floating-point contraction therefore answers what the split accumulation
71
+ answers -- usually the more accurate number, never the one a serial Ruby
72
+ loop gives. `CArray::JIT.reassociate = false`, or `CARRAY_JIT_REASSOCIATE=0`
73
+ for a whole process, asks for the serial order; `jit_contract` takes no
74
+ per-call licence. Integer contractions are unaffected.
75
+
76
+ - Change: `CArray.jit_contract` sums an index that repeats however often it
77
+ repeats, rather than refusing more than two positions. `q[i,i,i]` is the sum
78
+ along a cube's long diagonal, and `a[i,k] * b[k,k]` sums `k` at three
79
+ positions across two arrays. Nothing that compiled before compiles
80
+ differently: what changes is that these are accepted instead of raising
81
+ `CArray::JIT::Unsupported`.
82
+
83
+ - Fix: a contraction with nothing to assign into is collected into the type its
84
+ summand computes in. `CArray.jit_contract { |i, j, k| a[i,k] * b[k,j] }` over
85
+ float32 arrays came back int64 with every value truncated; over uint64 it came
86
+ back int64 and wrapped; over cmplx64 it raised. The assigned form -- the same
87
+ contraction written `c[i,j] = ...` -- was right throughout, and `jit_for`,
88
+ `jit_each` and `jit_stencil` were never affected.
89
+
90
+ - Fix: a contraction that writes an array it also reads is refused when the
91
+ block gave that array two names -- `y = x`, or a view of something being
92
+ read -- as it always was when one name was used for both. It compiled and
93
+ returned an answer that depended on the order the cells were reached in.
94
+ `CArray::JIT.contract_terms` refuses `into:` for the same reason. Write it
95
+ with `jit_for`, which is what a recurrence is for.
96
+
97
+ - Fix: `CArray.jit_stencil(source, into: source)` is refused rather than
98
+ computing a pass whose cells feed the ones after them. A window that reaches
99
+ nowhere -- one that reads only the cell it is on -- still writes in place, as
100
+ it always did.
101
+
102
+ - Fix: assigning to a loop index inside a kernel is refused. `jit_for(3) { |i|
103
+ i = 2; out[i] = ... }` assigned to the counter, so the loop walked somewhere
104
+ else -- outside the array, for a value outside its extent -- while Ruby reads
105
+ the same line as rebinding the parameter and runs the loop unchanged. Use a
106
+ local of another name; nothing that has an index only on the right changes.
107
+
108
+ - Fix: an index named after something the generated C already uses is refused
109
+ where it is written rather than by the compiler. `jit_contract { |int, j, k|
110
+ ... }` reached clang as a declaration of `int`, and an index named
111
+ `contraction` shared its identifier with the accumulator the compiler writes,
112
+ which made the sum come out zero with nothing said.
113
+
114
+ - Fix: a C function may take a `uint64_t *` and return a `uint64_t`.
115
+ `CArray.jit_function("void (*)(uint64_t *, int64_t)")` reached its cells as
116
+ an opaque slot rather than an array, and a `uint64_t` return type was
117
+ refused as "no value a compiled body can produce" -- from a table written
118
+ before uint64 was a type a kernel computes in. A `uint64_t` parameter taken
119
+ by value is still refused, and now says why: a value reaches a body as a
120
+ double, an int64 or a complex, and a uint64 fits none of them whole.
121
+
122
+ - Fix: `CArray.jit_map` collects a cmplx64 value into a cmplx64 array rather
123
+ than refusing to allocate one. Nothing else changes type: a block whose value
124
+ is cmplx128 still gives cmplx128.
125
+
40
126
  ## 0.1.1
41
127
 
42
128
  - Fix: a zero divisor in a `CArray.fuse` expression no longer turns
data/README.md CHANGED
@@ -8,13 +8,13 @@ The block is read with Prism, translated to C if it falls inside that subset, co
8
8
 
9
9
  ## Status
10
10
 
11
- 0.1.1 is the current release, and it still moves: behaviour can change between releases — see [CHANGELOG.md](CHANGELOG.md). A companion gem to CArray, it follows CArray's surface, which is not settled until CArray 3.1.
11
+ 0.1.2 is the current release, and it still moves: behaviour can change between releases — see [CHANGELOG.md](CHANGELOG.md). A companion gem to CArray, it follows CArray's surface, which is not settled until CArray 3.1.
12
12
 
13
13
  ## Features
14
14
 
15
15
  - **A JIT compiler for C-level loops.** A block becomes one C function over CArray's own memory, built by the system C compiler and called through Fiddle.
16
16
  - **Ordinary Ruby, and enough of it.** The source is parsed with Prism -- no DSL, no `eval` -- and every operation means what Ruby means by it, apart from the order a reduction takes its terms in. The subset is enough to state a numerical algorithm; what falls outside it is refused by name and line, not run as a Ruby loop.
17
- - **A method for each shape.** `jit_for` for recurrences and loops written out, `jit_stencil` for windows at any rank, `CArray.jit_contract` for contraction over a repeated index, `jit_each` and `jit_map` for a pass that reaches no neighbour.
17
+ - **A method for each shape.** `jit_for` for recurrences and loops written out, `jit_stencil` for windows at any rank, `CArray.jit_contract` for contraction over a repeated index, or over the indices left when the result's axes are named, `jit_each` and `jit_map` for a pass that reaches no neighbour.
18
18
  - **View- and mask-aware.** Columns, transposes and slices of slices are written in place without a copy, and masks propagate as CArray propagates them.
19
19
  - **Pure C functions, in and out.** `jit_extern` binds one from a library and a kernel calls it by address; `jit_function` compiles one from a block and hands back a C function pointer.
20
20
  - **The backend for `CArray.fuse`.** An array expression compiles instead of being walked a node at a time, without being asked and without changing the answer.
@@ -355,7 +355,7 @@ So this is not a faster `sum`. It is a way to write the reduction that has no `s
355
355
 
356
356
  ## Contraction
357
357
 
358
- `CArray.jit_contract` contracts over a repeated index: **an index that appears twice in the term is summed**. The repetition is the notation -- it is what stands in for the sigma.
358
+ `CArray.jit_contract` contracts over a repeated index: **an index that repeats in the term is summed**. The repetition is the notation -- it is what stands in for the sigma. How often it repeats does not enter into it: `q[i,i,i]` is one index read at three positions, and the sum runs along the cube's long diagonal.
359
359
 
360
360
  ```ruby
361
361
  c = CArray.jit_contract { |i, j, k| a[i,k] * b[k,j] } # a matrix product
@@ -384,14 +384,75 @@ Neither form decides what is summed. So a sum along an axis is not a contraction
384
384
  ```ruby
385
385
  CArray.jit_contract { |i, k| total[i] = a[i,k] }
386
386
  #=> `k` appears once, so it is free and must be on the left. A contraction
387
- # sums the indices that appear twice; to sum one that does not, write the
388
- # loop with jit_for, or use sum(axis:)
387
+ # sums the indices that repeat; to sum one that does not, write the loop
388
+ # with jit_for, or use sum(axis:)
389
389
  ```
390
390
 
391
391
  There is nothing in `a[i,k]` standing in for a sigma, and summing anyway would be the `=` quietly meaning something it does not say. `sum(axis: 1)` is that operation, and it is faster than anything written here.
392
392
 
393
- An array that is both written and read is a recurrence rather than a contraction, and is refused with a pointer at `jit_for` too.
393
+ ### Naming the result's axes
394
+
395
+ That a repeated index is summed is a statement about *dimensions*, which is the world the notation comes from: two dimensions met is an inner product, and there is no other reading. An index that numbers things -- a point, a sample, a batch -- is not a dimension. `x[p,k] * y[p,k]` repeating `p` says "the same point", not "sum over points", and the convention cannot tell the two apart. Naming the result's axes says which is meant:
396
+
397
+ ```ruby
398
+ n = CArray.jit_contract(:p) { |k| x[p,k] * y[p,k] } # one number per point
399
+ d = CArray.jit_contract(:a) { q[a,a] } # the diagonal, not the trace
400
+ r = CArray.jit_contract(:b, :i, :j) { |k| u[b,i,k] * v[b,k,j] } # a batch of products
401
+ ```
402
+
403
+ The arguments are the result's axes, in that order. What they say is which indices are free; they do not say what a repetition means, and a repetition still means a sum. So the whole of it is the convention's rule with a clause added:
404
+
405
+ **An index that repeats is summed; one that appears once is free; and a named one is free however often it appears.**
406
+
407
+ The third clause is what puts the per-point quantity and the diagonal inside the notation instead of outside it -- `q[a,a]` is the trace under the convention and the diagonal when the axis is named.
408
+
409
+ A free index then needs somewhere to go, and there are three places: the argument list, the left-hand side, or -- with neither -- the result's axes, which are the free indices in the order the block's parameters named them. So a parameter at a single position is refused once the axes are named. It is free, by the second clause, and the axes are already stated:
410
+
411
+ ```
412
+ `k` appears once, so it is free rather than summed. A contraction sums the
413
+ indices that repeat; name it as an axis of the result
414
+ (`CArray.jit_contract(:i, :k)`) to keep it, or use sum(axis:) to sum along
415
+ the axis
416
+ ```
417
+
418
+ So the list is all of the result's axes rather than some of them: name one and you have named them all. A partial one could be given a meaning -- the axes left out would take their order from the parameter list, as they do when nothing is named -- but it would only ever produce the orders that put the named axes first, so an order like `[i, b, j]` with `b` named could not be asked for at all. The result's order would be stated in two places, and neither could state all of it.
419
+
420
+ With every index named there is nothing left to sum, and the block takes no parameters at all.
421
+
422
+ This is the split `einsum` makes with `->`: `'ii'` is the trace and `'ii->i'` the diagonal, `'pk,pk'` is one number and `'pk,pk->p'` one per point. The argument list is that arrow's right-hand side.
423
+
424
+ Naming is allowed where the convention would have reached the same answer, which is how the result's axes are put in another order:
425
+
426
+ ```ruby
427
+ CArray.jit_contract(:j, :i) { |k| a[i,k] * b[k,j] } # the product, transposed
428
+ ```
429
+
430
+ An index cannot be both, and saying so twice is refused. Everything else is as it is under the convention: the extents come from the axes, an index whose axes disagree is refused, and assigning into an array of your own says where the result goes.
431
+
432
+
433
+
434
+ An array that is both written and read is a recurrence rather than a contraction, and is refused with a pointer at `jit_for` too -- under any name it was given. `y = x` is one array with two names, and so is a view of something being read; both are followed to the memory they share rather than compared by name.
435
+
436
+ ### The contraction without the block
437
+
438
+ A contraction that was *decided* rather than written has no block to read: a caller that rearranges one -- contracting two terms at a time, in an order it chose -- holds a structure, and a structure has no source. Two methods are the same contraction with the block taken out of the middle:
439
+
440
+ ```ruby
441
+ CArray::JIT.contraction_of(proc { |i, j, k| a[i,k] * b[k,j] })
442
+ #=> { :terms => [[a, [:i, :k]], [b, [:k, :j]]], :free => [:i, :j], :summed => [:k] }
443
+
444
+ CArray::JIT.contract_terms([[a, [:i, :k]], [b, [:k, :j]]], free: [:i, :j])
445
+ #=> the same matrix product
446
+ ```
447
+
448
+ `contract_terms` writes the block that was not written and compiles that, so the rules, the errors and the kernels are the ones above -- `free:` is the result's axes in order and is required, since a structure has no parameter list to say it in; an index that is not named is summed and must appear at more than one position; `into:` writes into an array of yours.
449
+
450
+ `contraction_of` returns nil when there is nothing to take apart: a summand that is more than a product of cells (`Math.exp(a[i,k]) * b[k,j]`, a division, a captured number, an index with an offset) or one that assigns into an array of its own. Nil is not a refusal -- `jit_contract` compiles all of those -- it says the block is not a product with pieces to rearrange.
451
+
452
+ Taken together they are what a contraction-order optimizer needs, and nothing else: a three-term product taken apart, contracted two at a time and put back agrees with the single nest to within the tolerance rearranging the additions earns, and on an 8x8x8x8 chain it is already faster.
394
453
 
395
454
  There is no BLAS for an arbitrary contraction, which is rather the point: this compiles to a plain nest of loops and is slower than a tuned GEMM, but it is one line and it exists.
396
455
 
397
- Its sum is serial. `jit_for`'s reduction takes partial sums by default and `jit_contract`'s does not, which is the wrong way round -- a contraction is a sum with no loop written anywhere for it to agree with -- and stays that way only until `jit_contract` has a meaning in the core to be licensed against.
456
+ Its sum is split into partial ones, as `jit_for`'s reduction is and as CArray's own reduce kernels are. A contraction is a sum with no loop written anywhere for its order to agree with -- the notation says which indices are summed and nothing about in what order -- so there is nothing being overridden, which is a weaker claim than the one `jit_for` makes over a loop somebody wrote. It is also the whole of the difference between a contraction and that loop: a 400 x 400 x 400 product took 47 ms serial against `jit_for`'s 16, and takes 16 split (`benchmark/contraction.rb`).
457
+
458
+ `CArray::JIT.reassociate = false`, and `CARRAY_JIT_REASSOCIATE=0` for a whole process, ask for the serial order here as they do everywhere -- which is the order a Ruby loop takes, and what to use to compare one against the other. Unlike `jit_for`, `jit_contract` has no per-call licence: a contraction names no loop, so there is no loop at the call site to license.
data/docs/04_Compiling.md CHANGED
@@ -89,6 +89,16 @@ process C, existing file dlopen 0.2 ms
89
89
 
90
90
  Set `CARRAY_JIT_NO_CACHE=1` (or `CARRAY_JIT_CACHE=none`) to put the cache in a temporary directory that is removed at exit, at that cost per kernel per run.
91
91
 
92
+ An application that would rather not share the cache under the home directory can name one of its own, before the first kernel is compiled:
93
+
94
+ ```ruby
95
+ CArray::JIT.cache_root = File.expand_path("../.jit-cache", __dir__)
96
+ ```
97
+
98
+ The path is expanded where it is given, so a relative one does not move when the program does. What is already compiled and loaded keeps working, and what is already on disk stays where it is; this says where the next kernel is looked for and written. A directory inside a project wants to be ignored by the version control it sits in.
99
+
100
+ `CARRAY_JIT_CACHE` and `CARRAY_JIT_NO_CACHE` still come first, so whoever runs the program has the last word on where a cache may be written and whether there is one at all. `CArray::JIT.cache_root = nil` restores the default.
101
+
92
102
  The cache is **bounded**: past `CARRAY_JIT_CACHE_LIMIT` kernels per environment (512 by default, so about 9 MB) the least recently used are evicted, source and object together. Reuse updates an entry's timestamp, so what a program actually runs stays. This is the one place carray-jit deliberately parts with RubyInline, whose `~/.ruby_inline` has no eviction at all and grows for the life of the account.
93
103
 
94
104
  Removing a cached object never breaks a kernel already in use: unlinking a loaded shared object leaves its mapping intact.
@@ -203,7 +213,7 @@ carray_jit_contiguous (char **pointers, int64_t *strides, int64_t *bounds, ...)
203
213
  | Variable | Effect |
204
214
  | --- | --- |
205
215
  | `CARRAY_JIT_DUMP` | Print generated C to stderr before compiling |
206
- | `CARRAY_JIT_CACHE` | Cache directory (default `~/.cache/carray-jit`), or `none` |
216
+ | `CARRAY_JIT_CACHE` | Cache directory (default `~/.cache/carray-jit`, or what `CArray::JIT.cache_root=` named), or `none` |
207
217
  | `CARRAY_JIT_NO_CACHE` | Keep the cache in a temporary directory, removed on exit |
208
218
  | `CARRAY_JIT_CACHE_LIMIT` | Kernels retained on disk, per environment (default 512) |
209
219
  | `CARRAY_JIT_CACHE_MAX_AGE_DAYS` | Days an unused environment's directory is kept (default 30) |
@@ -13,7 +13,8 @@ installed. Every example here runs as written.
13
13
  | The same, and you want the result back | `CArray.jit_map` |
14
14
  | A cell reads its neighbours, or the one computed before it | `CArray.jit_for` |
15
15
  | A cell reads a window, and the edge needs a rule | `CArray.jit_stencil` |
16
- | An index appears twice and is summed | `CArray.jit_contract` |
16
+ | An index repeats and is summed | `CArray.jit_contract` |
17
+ | An index repeats and is *not* summed -- a point number, a batch | `CArray.jit_contract(:p)`, naming the result's axes |
17
18
  | Call a C function someone else compiled | `CArray.jit_extern` |
18
19
  | Compile a C function of your own | `CArray.jit_function` |
19
20
 
@@ -91,12 +92,22 @@ is what the cell gets.
91
92
  ```ruby
92
93
  c = CArray.jit_contract { |i, j, k| a[i,k] * b[k,j] } # a matrix product
93
94
  CArray.jit_contract { |i, j, k| c[i,j] = a[i,k] * b[k,j] }
95
+ n = CArray.jit_contract(:p) { |k| x[p,k] * y[p,k] } # the axes named: one per point
96
+ d = CArray.jit_contract(:a) { q[a,a] } # the diagonal, not the trace
94
97
  ```
95
98
 
96
- **An index that appears twice is summed.** One that appears once is free and
97
- becomes an axis of the result, in the order the block named them -- so
98
- `{ |j, i, k| ... }` is the transpose. No extent is given: each index's extent
99
- comes from the axes it addresses.
99
+ **An index that repeats is summed**, however often it repeats. One that appears
100
+ once is free and becomes an axis of the result, in the order the block named
101
+ them -- so `{ |j, i, k| ... }` is the transpose. **An index named in the
102
+ arguments is free however often it appears**, which is the third clause and
103
+ what puts the diagonal and the per-point quantity here rather than in a loop.
104
+
105
+ Naming one axis names them all: the argument list is the result's axes and
106
+ their order, so a parameter left at a single position is refused there as it is
107
+ under the convention. No extent is given: each index's extent comes from the
108
+ axes it addresses. The sum is split into partial sums, as `jit_for`'s reduction
109
+ is -- `CArray::JIT.reassociate = false` is the serial order, and a contraction
110
+ has no per-call licence of its own.
100
111
 
101
112
  Assigning into an array of yours says where to put it and in what order its
102
113
  axes lie. It does **not** decide what is summed -- so `total[i] = a[i,k]` is
@@ -155,7 +166,7 @@ that was not asked.
155
166
  | `jit_stencil` with no array given | the arrays are arguments, not closures |
156
167
  | `jit_stencil` with both `type:` and `into:` | `into:` already decides the type |
157
168
  | a contraction summing an index that appears once | not the convention; `sum(axis:)` |
158
- | an index appearing more than twice | there is no pair to sum |
169
+ | a parameter at one position once the axes are named | it is free, and the axes are already stated |
159
170
  | an index whose axes disagree in extent | the shape check a contraction exists to do |
160
171
  | an array both written and read in a contraction | a recurrence -- write it with `jit_for` |
161
172
  | a block naming a construct outside the subset | refused by name and line |
@@ -163,10 +174,11 @@ that was not asked.
163
174
  ## Knobs
164
175
 
165
176
  ```ruby
166
- CArray::JIT.reassociate #=> true, the default for jit_for reductions
177
+ CArray::JIT.reassociate #=> true, for jit_for reductions and every contraction
167
178
  CArray::JIT.reassociate = false # serial accumulation everywhere
168
179
 
169
180
  CArray::JIT.cache_directory #=> ~/.cache/carray-jit/<version>
181
+ CArray::JIT.cache_root = "tmp/jit" # this application keeps its own
170
182
  CArray::JIT.cache_entry_count
171
183
  CArray::JIT.cache_byte_size
172
184
  CArray::JIT.clear_cache
@@ -1,6 +1,6 @@
1
1
  # Rotating a point cloud, then projecting it onto a basis
2
2
  #
3
- # Both are sums over an index that appears twice, which is what
3
+ # Both are sums over an index that repeats, which is what
4
4
  # `CArray.jit_contract` is: the repeated index is summed, so the notation is the
5
5
  # formula.
6
6
  #
@@ -8,6 +8,11 @@
8
8
  # cov[a,b] = sum_p c[p,a] c[p,b] the covariance of the cloud
9
9
  # coeff[p,m] = sum_k x[p,k] basis[m,k] project onto a basis
10
10
  # recon[p,k] = sum_m coeff[p,m] basis[m,k] and build the points back
11
+ # norm[p] = sum_k x[p,k] x[p,k] how far each point is out
12
+ #
13
+ # The last one is where the convention needs help: p appears twice there as
14
+ # well, but it numbers the points rather than naming a dimension, so it is
15
+ # named as the result's axis and only k is summed.
11
16
  #
12
17
  # Written as loops these are three lines each and easy to get subtly wrong: an
13
18
  # index in the wrong place transposes the answer rather than failing. Here the
@@ -69,18 +74,21 @@ puts format(" dropping the third direction costs %.2e per point", residual)
69
74
  puts format(" which is the variance that was in it: %.2e",
70
75
  rotated[nil, 2].stddev ** 2)
71
76
 
72
- # The distance of every point from the origin is *not* a contraction, and this
73
- # is the place the convention bites: in `x[p,k] * x[p,k]` the index p appears
74
- # twice as well, so it would be summed too and the answer would be one number.
75
- # A quantity per point is a per-cell loop, and says so.
76
- squared = CArray.double(count)
77
- CArray.jit_for(count) { |p|
78
- total = 0.0
79
- (0...3).each { |k| total = total + rotated[p, k] * rotated[p, k] }
80
- squared[p] = total
81
- }
77
+ # The distance of every point from the origin, and the place the convention
78
+ # alone would say the wrong thing: in `x[p,k] * x[p,k]` the index p appears
79
+ # twice, so it would be summed as well and the answer would be one number for
80
+ # the whole cloud. p is not a dimension, though -- repeating it says "the
81
+ # same point" -- so it is named as the result's axis, and then k is the only
82
+ # index left to sum.
83
+ squared = CArray.jit_contract(:p) { |k| rotated[p,k] * rotated[p,k] }
82
84
  puts format(" furthest point %.4f away", Math.sqrt(squared.max))
83
85
 
86
+ # Name nothing and the same term is the other reading, which is the number
87
+ # the per-point one adds up to.
88
+ whole = CArray.jit_contract { |p, k| rotated[p,k] * rotated[p,k] }
89
+ puts format(" the cloud's total %.1f, and per point summed again %.1f",
90
+ whole[0], squared.sum)
91
+
84
92
  # The same rotation written as a Ruby loop.
85
93
  reference = CArray.double(count, 3)
86
94
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -99,6 +107,10 @@ compiled = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
99
107
 
100
108
  puts format(" rotation: %.1f ms compiled, %.0f ms as a Ruby loop (%.0fx)",
101
109
  compiled * 1e3, interpreted * 1e3, interpreted / compiled)
110
+ # The two agree to the last bit here, which is not the general case: a kernel
111
+ # splits its sum into partial ones, and only the serial loop's order gives the
112
+ # serial loop's answer. The sum is over j, whose extent is three, so the eight
113
+ # partial sums never fill a round and what runs is the serial tail.
102
114
  puts " identical: #{rotated.to_a == reference.to_a}"
103
115
 
104
116
  # An index whose axes disagree is the mistake this notation exists to catch.