carray-jit 0.1.2 → 0.1.3

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +771 -3
  3. data/README.md +7 -6
  4. data/carray-jit.gemspec +1 -3
  5. data/docs/00_Introduction.md +4 -3
  6. data/docs/01_GettingStarted.md +1 -1
  7. data/docs/02_KernelShapes.md +93 -14
  8. data/docs/03_SupportedFeatures.md +582 -26
  9. data/docs/04_Compiling.md +33 -6
  10. data/docs/05_DesignNotes.md +3 -3
  11. data/docs/06_Cheatsheet.md +198 -5
  12. data/docs/07_StepByStep.ja.md +534 -0
  13. data/docs/07_StepByStep.md +535 -0
  14. data/examples/README.md +12 -0
  15. data/examples/applications/alarm.rb +121 -0
  16. data/examples/applications/collatz.rb +105 -0
  17. data/examples/applications/cubic_spline.rb +331 -0
  18. data/examples/applications/dithering.rb +144 -0
  19. data/examples/applications/group_stats.rb +115 -0
  20. data/examples/applications/lookup.rb +126 -0
  21. data/examples/applications/median_filter.rb +153 -0
  22. data/examples/applications/parcel_ascent.rb +220 -0
  23. data/examples/applications/point_in_polygon.rb +111 -0
  24. data/examples/applications/random_walk.rb +98 -0
  25. data/examples/applications/van_der_pol.rb +186 -0
  26. data/examples/applications/wet_bulb.rb +140 -0
  27. data/examples/features/10_complex.rb +14 -4
  28. data/examples/features/15_loops.rb +7 -1
  29. data/lib/carray/jit/access.rb +14 -0
  30. data/lib/carray/jit/analyzer.rb +2077 -136
  31. data/lib/carray/jit/block_reader.rb +37 -6
  32. data/lib/carray/jit/c_function.rb +613 -76
  33. data/lib/carray/jit/c_generator.rb +1595 -156
  34. data/lib/carray/jit/call.rb +68 -0
  35. data/lib/carray/jit/compiler.rb +75 -11
  36. data/lib/carray/jit/kernel.rb +369 -32
  37. data/lib/carray/jit/node.rb +359 -9
  38. data/lib/carray/jit/sorting_networks.rb +182 -0
  39. data/lib/carray/jit/type_assignment.rb +371 -34
  40. data/lib/carray/jit/version.rb +1 -1
  41. data/lib/carray/jit.rb +560 -64
  42. metadata +22 -8
  43. data/ext/carray_jit_access/carray_jit_access.c +0 -460
  44. data/ext/carray_jit_access/extconf.rb +0 -8
@@ -0,0 +1,98 @@
1
+ # Gambler's ruin: how long a random walk lasts, and which end it stops at.
2
+ #
3
+ # A walk steps up or down by one until it reaches -A or +B. How many steps
4
+ # that takes is different every time and is not bounded by anything, so this
5
+ # is `while` again -- but the new part is where the steps come from. A kernel
6
+ # runs with the GVL released, so Ruby's `rand` is not reachable from inside
7
+ # one; what is reachable is `CArray::Rng`, whose C the compiler pastes into
8
+ # the kernel. One draw per step, in the loop, at no call.
9
+ #
10
+ # The answers are known exactly, which is what makes this worth running: a
11
+ # walk started at 0 between -A and +B lasts A*B steps on average and stops at
12
+ # +B with probability A/(A+B). A hundred thousand walks should land near
13
+ # both.
14
+ #
15
+ # ruby examples/applications/random_walk.rb
16
+
17
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
18
+ require "carray/jit"
19
+
20
+ WALKS = 200_000
21
+ DOWN, UP = -8, 12 # the two barriers
22
+
23
+ def walk (n, seed)
24
+ random = CArray::Rng.new(seed: seed)
25
+ steps = CArray.int32(n)
26
+ ended = CArray.int8(n) # 1 when it stopped at the top
27
+ CArray.jit_for(n) { |i|
28
+ position = 0
29
+ taken = 0
30
+ while position > DOWN && position < UP
31
+ position = random.random < 0.5 ? position - 1 : position + 1
32
+ taken = taken + 1
33
+ end
34
+ steps[i] = taken
35
+ ended[i] = position == UP ? 1 : 0
36
+ }
37
+ [steps, ended]
38
+ end
39
+
40
+ steps, ended = walk(WALKS, 20260913)
41
+
42
+ puts format("%d walks between %d and %d", WALKS, DOWN, UP)
43
+ puts format(" mean length %8.2f (exactly %d)",
44
+ steps.mean, -DOWN * UP)
45
+ puts format(" stopped at the top %8.4f (exactly %.4f)",
46
+ ended.sum.to_f / WALKS, -DOWN.to_f / (UP - DOWN))
47
+ puts format(" longest walk %8d steps", steps.max)
48
+
49
+ # The shape of it, which no formula on the line above shows.
50
+ puts
51
+ puts "how long they lasted"
52
+ edges = [0, 25, 50, 100, 200, 400, 800, 1 << 30]
53
+ edges.each_cons(2) do |low, high|
54
+ count = steps.ge(low).and(steps.lt(high)).count(1)
55
+ width = 60 * count / WALKS
56
+ bar = "#" * (count.positive? ? [width, 1].max : 0)
57
+ label = high > WALKS ? format("%4d+", low) : format("%4d-%-4d", low, high - 1)
58
+ puts format(" %-10s %6d %s", label, count, bar)
59
+ end
60
+
61
+ # A draw is not fixed to a cell. Which draw a cell gets is the order the loop
62
+ # ran in, so the same seed through the same kernel repeats exactly -- and if a
63
+ # cell has to get *its own* draw whatever the loop does, the way to say that
64
+ # is to fill an array with `CArray#random!` first and read a cell of it.
65
+ again, = walk(WALKS, 20260913)
66
+ puts
67
+ puts format("the same seed gives the same walks %s", steps.to_a == again.to_a)
68
+
69
+ # The same thing in Ruby, which is where a Monte Carlo usually starts life.
70
+ def walk_in_ruby (n, seed)
71
+ random = Random.new(seed)
72
+ steps = Array.new(n, 0)
73
+ n.times do |i|
74
+ position = 0
75
+ taken = 0
76
+ while position > DOWN && position < UP
77
+ position = random.rand < 0.5 ? position - 1 : position + 1
78
+ taken += 1
79
+ end
80
+ steps[i] = taken
81
+ end
82
+ steps
83
+ end
84
+
85
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
86
+ 3.times { walk(WALKS, 20260913) }
87
+ compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 3
88
+
89
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
90
+ in_ruby = walk_in_ruby(WALKS, 20260913)
91
+ interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
92
+
93
+ puts
94
+ puts format("%d walks, about %d draws in all", WALKS, steps.sum)
95
+ puts format(" %6.1f ms compiled, %6.0f ms in Ruby (%.0fx)",
96
+ compiled * 1e3, interpreted * 1e3, interpreted / compiled)
97
+ puts format(" Ruby's own walks average %.2f steps, which is the same law",
98
+ in_ruby.sum.to_f / WALKS)
@@ -0,0 +1,186 @@
1
+ # An adaptive integrator, where the step size is the trajectory's own.
2
+ #
3
+ # The van der Pol oscillator is stiff in proportion to its parameter: at
4
+ # mu = 0.1 it is a gentle circle, at mu = 40 it spends most of the cycle
5
+ # crawling and the rest of it snapping across. An integrator worth using
6
+ # answers that by changing its step -- taking it as long as the error allows,
7
+ # shortening it and *doing the step again* when the estimate comes back too
8
+ # large.
9
+ #
10
+ # Neither half of that survives an expression over whole arrays. A fixed step
11
+ # can be written as an array pass, but then every trajectory pays the stiffest
12
+ # one's step count; and a rejected step -- this trajectory redoing what it
13
+ # just did while the others move on -- has no array spelling at all. Here the
14
+ # step, the retry and the count belong to the cell.
15
+ #
16
+ # The method is Bogacki-Shampine 3(2): four stages, the third-order answer
17
+ # carried forward and the second-order one used to size the error.
18
+ #
19
+ # ruby examples/applications/van_der_pol.rb
20
+
21
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
22
+ require "carray/jit"
23
+
24
+ TRAJECTORIES = 2_000
25
+ FINISH = 20.0
26
+ TOLERANCE = 1.0e-8
27
+ CAP = 200_000 # a `while` in a kernel carries a bound
28
+
29
+ stiffness = CArray.double(TRAJECTORIES).seq!(0.1, 40.0 / TRAJECTORIES)
30
+ position = CArray.double(TRAJECTORIES)
31
+ velocity = CArray.double(TRAJECTORIES)
32
+ accepted = CArray.int32(TRAJECTORIES)
33
+ rejected = CArray.int32(TRAJECTORIES)
34
+
35
+ def integrate (stiffness, position, velocity, accepted, rejected)
36
+ CArray.jit_for(stiffness.elements) { |i|
37
+ mu = stiffness[i]
38
+ x = 2.0 # the trajectory starts out here
39
+ v = 0.0
40
+ t = 0.0
41
+ step = 0.01
42
+ taken = 0
43
+ thrown = 0
44
+ while t < FINISH && taken < CAP
45
+ step = FINISH - t if t + step > FINISH
46
+
47
+ k1x = v
48
+ k1v = mu * (1.0 - x * x) * v - x
49
+ x2 = x + 0.5 * step * k1x
50
+ v2 = v + 0.5 * step * k1v
51
+ k2x = v2
52
+ k2v = mu * (1.0 - x2 * x2) * v2 - x2
53
+ x3 = x + 0.75 * step * k2x
54
+ v3 = v + 0.75 * step * k2v
55
+ k3x = v3
56
+ k3v = mu * (1.0 - x3 * x3) * v3 - x3
57
+ nx = x + step * (2.0 * k1x + 3.0 * k2x + 4.0 * k3x) / 9.0
58
+ nv = v + step * (2.0 * k1v + 3.0 * k2v + 4.0 * k3v) / 9.0
59
+ k4x = nv
60
+ k4v = mu * (1.0 - nx * nx) * nv - nx
61
+
62
+ ex = step * (-5.0 * k1x / 72.0 + k2x / 12.0 + k3x / 9.0 - k4x / 8.0)
63
+ ev = step * (-5.0 * k1v / 72.0 + k2v / 12.0 + k3v / 9.0 - k4v / 8.0)
64
+ error = ex.abs > ev.abs ? ex.abs : ev.abs
65
+ allowed = TOLERANCE * (1.0 + (nx.abs > nv.abs ? nx.abs : nv.abs))
66
+
67
+ if error <= allowed
68
+ t = t + step
69
+ x = nx
70
+ v = nv
71
+ taken = taken + 1
72
+ else
73
+ # The step is not taken: this trajectory does it again, shorter,
74
+ # while every other one carries on from wherever it is.
75
+ thrown = thrown + 1
76
+ end
77
+
78
+ growth = 0.9 * Math.exp(Math.log(allowed / (error + 1.0e-300)) / 3.0)
79
+ growth = 5.0 if growth > 5.0
80
+ growth = 0.2 if growth < 0.2
81
+ step = step * growth
82
+ end
83
+ position[i] = x
84
+ velocity[i] = v
85
+ accepted[i] = taken
86
+ rejected[i] = thrown
87
+ }
88
+ end
89
+
90
+ integrate(stiffness, position, velocity, accepted, rejected)
91
+
92
+ puts format("%d trajectories, mu from %.1f to %.1f, integrated to t = %.0f",
93
+ TRAJECTORIES, stiffness[0], stiffness[TRAJECTORIES - 1], FINISH)
94
+ puts format(" steps taken %d at the least, %d at the most, %.0f on average",
95
+ accepted.min, accepted.max, accepted.mean)
96
+ puts format(" steps thrown away up to %d on one trajectory", rejected.max)
97
+ puts format(" a fixed step would have to be the stiffest one's, so %d for everybody",
98
+ accepted.max)
99
+ puts format(" which is %.1f times the work this did", accepted.max.to_f / accepted.mean)
100
+
101
+ # Against a reference: the same trajectory by fixed-step RK4, fine enough that
102
+ # it is the answer rather than another approximation.
103
+ def reference (mu, finish, steps)
104
+ x = 2.0
105
+ v = 0.0
106
+ step = finish / steps
107
+ steps.times do
108
+ k1x = v; k1v = mu * (1.0 - x * x) * v - x
109
+ ax = x + 0.5 * step * k1x; av = v + 0.5 * step * k1v
110
+ k2x = av; k2v = mu * (1.0 - ax * ax) * av - ax
111
+ bx = x + 0.5 * step * k2x; bv = v + 0.5 * step * k2v
112
+ k3x = bv; k3v = mu * (1.0 - bx * bx) * bv - bx
113
+ cx = x + step * k3x; cv = v + step * k3v
114
+ k4x = cv; k4v = mu * (1.0 - cx * cx) * cv - cx
115
+ x += step * (k1x + 2 * k2x + 2 * k3x + k4x) / 6.0
116
+ v += step * (k1v + 2 * k2v + 2 * k3v + k4v) / 6.0
117
+ end
118
+ x
119
+ end
120
+
121
+ puts
122
+ puts " mu here reference diff steps"
123
+ [0, TRAJECTORIES / 2, TRAJECTORIES - 1].each do |i|
124
+ exact = reference(stiffness[i], FINISH, 200_000)
125
+ puts format(" %5.1f %8.5f %10.5f %10.2e %6d",
126
+ stiffness[i], position[i], exact, (position[i] - exact).abs, accepted[i])
127
+ end
128
+
129
+ # Ruby does the same adaptive integration, over as many trajectories as is
130
+ # polite to wait for.
131
+ SAMPLE = 100
132
+
133
+ def integrate_in_ruby (stiffness, count)
134
+ count.times.map do |i|
135
+ mu = stiffness[i]
136
+ x = 2.0
137
+ v = 0.0
138
+ t = 0.0
139
+ step = 0.01
140
+ taken = 0
141
+ while t < FINISH && taken < CAP
142
+ step = FINISH - t if t + step > FINISH
143
+ k1x = v; k1v = mu * (1.0 - x * x) * v - x
144
+ x2 = x + 0.5 * step * k1x; v2 = v + 0.5 * step * k1v
145
+ k2x = v2; k2v = mu * (1.0 - x2 * x2) * v2 - x2
146
+ x3 = x + 0.75 * step * k2x; v3 = v + 0.75 * step * k2v
147
+ k3x = v3; k3v = mu * (1.0 - x3 * x3) * v3 - x3
148
+ nx = x + step * (2.0 * k1x + 3.0 * k2x + 4.0 * k3x) / 9.0
149
+ nv = v + step * (2.0 * k1v + 3.0 * k2v + 4.0 * k3v) / 9.0
150
+ k4x = nv; k4v = mu * (1.0 - nx * nx) * nv - nx
151
+ ex = step * (-5.0 * k1x / 72.0 + k2x / 12.0 + k3x / 9.0 - k4x / 8.0)
152
+ ev = step * (-5.0 * k1v / 72.0 + k2v / 12.0 + k3v / 9.0 - k4v / 8.0)
153
+ error = [ex.abs, ev.abs].max
154
+ allowed = TOLERANCE * (1.0 + [nx.abs, nv.abs].max)
155
+ if error <= allowed
156
+ t += step
157
+ x = nx
158
+ v = nv
159
+ taken += 1
160
+ end
161
+ growth = 0.9 * (allowed / (error + 1.0e-300)) ** (1.0 / 3.0)
162
+ growth = 5.0 if growth > 5.0
163
+ growth = 0.2 if growth < 0.2
164
+ step *= growth
165
+ end
166
+ x
167
+ end
168
+ end
169
+
170
+ in_ruby = integrate_in_ruby(stiffness, SAMPLE)
171
+ agree = (0...SAMPLE).all? { |i| (position[i] - in_ruby[i]).abs < 1.0e-12 }
172
+
173
+ def timed (repeats = 3)
174
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
175
+ repeats.times { yield }
176
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / repeats
177
+ end
178
+
179
+ compiled = timed { integrate(stiffness, position, velocity, accepted, rejected) }
180
+ interpreted = timed(1) { integrate_in_ruby(stiffness, SAMPLE) }
181
+
182
+ puts
183
+ puts format(" agrees with the same integrator in Ruby %s", agree)
184
+ puts format(" %6.0f ms here for %d trajectories, %6.0f ms in Ruby for %d (%.0fx a trajectory)",
185
+ compiled * 1e3, TRAJECTORIES, interpreted * 1e3, SAMPLE,
186
+ (interpreted / SAMPLE) / (compiled / TRAJECTORIES))
@@ -0,0 +1,140 @@
1
+ # A derived field on a weather grid, where one of the fields needs solving.
2
+ #
3
+ # Temperature and relative humidity come off a grid -- a GRIB message, a
4
+ # netCDF variable, a model's output -- and what is wanted is something else.
5
+ # The dew point is an expression: saturation vapour pressure, the actual
6
+ # vapour pressure from the humidity, and Magnus inverted. Written over whole
7
+ # arrays that is four passes and three temporaries the size of the grid;
8
+ # written as a kernel it is one pass and none, which is the ordinary reason
9
+ # to use one.
10
+ #
11
+ # The wet-bulb temperature is the other kind. It is defined by the
12
+ # psychrometric equation, which has no closed form, so every cell solves its
13
+ # own -- and how many Newton steps that takes depends on how far the cell's
14
+ # dew point is from its answer, which is the cell's own business. The
15
+ # whole-array route can still be written, because a fixed number of rounds
16
+ # covers the worst cell; what it cannot do is let a cell that has converged
17
+ # stop, so every cell pays the worst one's count, and each round is a pass
18
+ # over the grid holding another grid.
19
+ #
20
+ # ruby examples/applications/wet_bulb.rb
21
+
22
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
23
+ require "carray/jit"
24
+
25
+ ROWS, COLUMNS = 721, 1440 # a quarter-degree global grid
26
+ PRESSURE = 1013.25 # hPa, one level
27
+ TOLERANCE = 1.0e-6 # degrees, how close is close enough
28
+ CAP = 50 # a `while` in a kernel takes a cap
29
+
30
+ # Stand-ins for the fields that would have been read: a pole-to-pole
31
+ # temperature gradient with a wave in it, and humidity that varies across it.
32
+ temperature = CArray.double(ROWS, COLUMNS)
33
+ humidity = CArray.double(ROWS, COLUMNS)
34
+ CArray.jit_for(ROWS, COLUMNS) { |j, i|
35
+ latitude = 90.0 - 180.0 * j / (ROWS - 1)
36
+ temperature[j, i] = 30.0 - 55.0 * (latitude / 90.0).abs +
37
+ 4.0 * Math.sin(i / 40.0) * Math.cos(latitude / 25.0)
38
+ humidity[j, i] = 35.0 + 60.0 * (0.5 + 0.5 * Math.cos(i / 30.0 + j / 60.0))
39
+ }
40
+
41
+ dew_point = CArray.double(ROWS, COLUMNS)
42
+ wet_bulb = CArray.double(ROWS, COLUMNS)
43
+ rounds_taken = CArray.int8(ROWS, COLUMNS)
44
+
45
+ # Everything in one walk. `vapour` and `gamma` are the cell's own values and
46
+ # never become arrays; `low` and `high` are the cell's bracket, which is why
47
+ # the loop can end when this cell is done rather than when the last one is.
48
+ def derive (temperature, humidity, dew_point, wet_bulb, rounds_taken)
49
+ rows, columns = temperature.dim
50
+ CArray.jit_for(rows, columns) { |j, i|
51
+ air = temperature[j, i]
52
+ saturation = 6.112 * Math.exp(17.67 * air / (air + 243.5))
53
+ vapour = saturation * humidity[j, i] / 100.0
54
+ gamma = Math.log(vapour / 6.112)
55
+ dew = 243.5 * gamma / (17.67 - gamma)
56
+ dew_point[j, i] = dew
57
+
58
+ # Newton on the psychrometric equation, started from the dew point. The
59
+ # cap is not decoration: a `while` in a kernel has no interpreter to
60
+ # interrupt it, so a solver carries the bound it is allowed.
61
+ guess = dew
62
+ step = 1.0
63
+ taken = 0
64
+ while step.abs > TOLERANCE && taken < CAP
65
+ saturated = 6.112 * Math.exp(17.67 * guess / (guess + 243.5))
66
+ residual = saturated - 0.000665 * PRESSURE * (air - guess) - vapour
67
+ slope = saturated * 17.67 * 243.5 / ((guess + 243.5) * (guess + 243.5)) +
68
+ 0.000665 * PRESSURE
69
+ step = residual / slope
70
+ guess = guess - step
71
+ taken = taken + 1
72
+ end
73
+ wet_bulb[j, i] = guess
74
+ rounds_taken[j, i] = taken
75
+ }
76
+ end
77
+
78
+ derive(temperature, humidity, dew_point, wet_bulb, rounds_taken)
79
+
80
+ puts format("%d x %d grid, %d cells", ROWS, COLUMNS, ROWS * COLUMNS)
81
+ puts " T RH Td Tw"
82
+ [[180, 0], [360, 200], [360, 720], [540, 1000], [700, 30]].each do |j, i|
83
+ puts format(" %6.2f %6.1f %8.2f %8.2f",
84
+ temperature[j, i], humidity[j, i], dew_point[j, i], wet_bulb[j, i])
85
+ end
86
+ puts format(" the wet bulb is never above the air temperature %s",
87
+ (wet_bulb - temperature).max <= 0.0)
88
+ puts format(" and never below the dew point %s",
89
+ (wet_bulb - dew_point).min >= -TOLERANCE)
90
+
91
+ # The part an expression cannot have: the cells did not agree on how long
92
+ # this took.
93
+ puts
94
+ puts format("Newton steps per cell: %d at the least, %d at the most, %.2f on average",
95
+ rounds_taken.min, rounds_taken.max, rounds_taken.mean)
96
+
97
+ # The same Newton iteration over whole arrays. It cannot stop per cell, so it runs
98
+ # the worst cell's count everywhere, and every line here is a pass over the
99
+ # grid holding another grid.
100
+ def derive_over_arrays (temperature, humidity, rounds)
101
+ saturation = (temperature * 17.67 / (temperature + 243.5)).exp * 6.112
102
+ vapour = saturation * humidity / 100.0
103
+ gamma = (vapour / 6.112).log
104
+ dew = gamma * 243.5 / (17.67 - gamma)
105
+ guess = dew.to_ca
106
+ rounds.times do
107
+ saturated = (guess * 17.67 / (guess + 243.5)).exp * 6.112
108
+ residual = saturated - (temperature - guess) * (0.000665 * PRESSURE) - vapour
109
+ slope = saturated * (17.67 * 243.5) / ((guess + 243.5) * (guess + 243.5)) +
110
+ 0.000665 * PRESSURE
111
+ guess = guess - residual / slope
112
+ end
113
+ [dew, guess]
114
+ end
115
+
116
+ rounds = rounds_taken.max
117
+ array_dew, array_wet = derive_over_arrays(temperature, humidity, rounds)
118
+ puts format(" over whole arrays, everyone pays %d rounds", rounds)
119
+ puts format(" and agrees with the kernel to %.1e %s", TOLERANCE,
120
+ (array_wet - wet_bulb).abs.max < TOLERANCE)
121
+
122
+ def timed (repeats = 3)
123
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
124
+ repeats.times { yield }
125
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / repeats
126
+ end
127
+
128
+ compiled = timed { derive(temperature, humidity, dew_point, wet_bulb, rounds_taken) }
129
+ whole = timed { derive_over_arrays(temperature, humidity, rounds) }
130
+
131
+ puts
132
+ puts format(" one kernel %7.0f ms", compiled * 1e3)
133
+ puts format(" whole arrays %7.0f ms %.1fx", whole * 1e3, whole / compiled)
134
+ puts format(" %.2f Newton steps a cell here, %d for every cell there", rounds_taken.mean, rounds)
135
+ # Each array operation in `derive_over_arrays` is its own walk over the grid
136
+ # and its own array to hold the answer -- sixteen of them per round, counted
137
+ # off the expression -- where the kernel walks the grid once and holds the
138
+ # cell's `guess` in a register.
139
+ puts format(" the grid is %.1f MB, and the array route builds %d of those and walks the grid %d times",
140
+ ROWS * COLUMNS * 8 / 1048576.0, 16 * rounds, 16 * rounds)
@@ -48,15 +48,25 @@ CArray.jit_for(n) { |i| transformed[i] = signal[i].tanh }
48
48
  puts " tanh matches the array operator #{transformed.to_a == signal.tanh.to_a}"
49
49
 
50
50
  # A reduction over complex cells: the accumulator starts from a complex zero.
51
+ # The sum is split into partial ones, as every reduction here is, so the last
52
+ # bits are not the serial loop's -- `reassociate: false` asks for that order
53
+ # and gets it exactly.
51
54
  total = CArray.cmplx128(1)
52
55
  CArray.jit_for(1) { |i|
53
56
  running = Complex(0.0, 0.0)
54
57
  (0...n).each { |j| running = running + signal[j] }
55
58
  total[i] = running
56
59
  }
60
+ in_order = CArray.cmplx128(1)
61
+ CArray.jit_for(1, reassociate: false) { |i|
62
+ running = Complex(0.0, 0.0)
63
+ (0...n).each { |j| running = running + signal[j] }
64
+ in_order[i] = running
65
+ }
57
66
  serial = (0...n).inject(Complex(0.0, 0.0)) { |sum, j| sum + signal[j] }
58
67
  puts " sum #{total[0].rectangular.map { |e| e.round(6) }.inspect}"
59
- puts " matches Ruby #{total[0] == serial}"
68
+ puts " split sum matches Ruby #{total[0] == serial} <- the partial sums"
69
+ puts " serial sum matches Ruby #{in_order[0] == serial} <- reassociate: false"
60
70
 
61
71
  # Where Ruby and C part company. A real operand carries an exact Integer zero
62
72
  # as its imaginary part, and Ruby's own arithmetic returns the other operand
@@ -72,8 +82,8 @@ puts " Ruby #{(Complex(1.0, -0.0) + 2.0)}"
72
82
  puts " the kernel #{kept[0]}"
73
83
  puts " widening first #{Complex(1.0, -0.0) + Complex(2.0, 0.0)} <- what C's + would give"
74
84
 
75
- # `**` is the one operation whose answer is not the Ruby loop's to the last
76
- # bit: Ruby raises a Complex to a power by binary powering, cpow goes round
85
+ # Once the order is the same, `**` is the one operation whose answer is still
86
+ # not the Ruby loop's to the last bit: Ruby raises a Complex to a power by binary powering, cpow goes round
77
87
  # through exp and log. A few machine epsilons apart, growing with the
78
88
  # exponent -- and `z * z` is both exact and cheaper than the library call.
79
89
  cubed = CArray.cmplx128(n)
@@ -84,7 +94,7 @@ squared = CArray.cmplx128(n)
84
94
  CArray.jit_for(n) { |i| squared[i] = signal[i] * signal[i] }
85
95
 
86
96
  puts
87
- puts "the one inexact operation"
97
+ puts "the one inexact operation, the order being equal"
88
98
  puts " z ** 3 worst #{(worst / Float::EPSILON).round(1)} machine epsilons"
89
99
  puts " z * z matches Ruby exactly #{(0...n).all? { |i| squared[i] == signal[i] * signal[i] }}"
90
100
 
@@ -61,7 +61,13 @@ CArray.jit_for(n) { |i|
61
61
 
62
62
  puts
63
63
  puts "Newton, bounded by the extent"
64
- puts format(" worst relative error %.1e", ((root ** 2 - a).abs / a).max)
64
+ # Against Math.sqrt rather than against the loop's own residual: the residual
65
+ # is what the break tested, so it can only come back at the tolerance.
66
+ exact = CArray.double(n) { |i| Math.sqrt(a[i]) }
67
+ puts format(" worst relative error %.1e (vs Math.sqrt)",
68
+ ((root - exact).abs / exact).max)
69
+ puts format(" worst residual %.1e (what the break tested)",
70
+ ((root ** 2 - a).abs / a).max)
65
71
  puts " passes taken #{passes.min}..#{passes.max}"
66
72
  puts " none ran out #{passes.max < cap}"
67
73
 
@@ -0,0 +1,14 @@
1
+ # The address basis a kernel is handed -- a pointer, and one byte stride per
2
+ # axis, lent for the length of a block -- is knowledge about CArray's views
3
+ # written against CArray's own C, and it lives there. This gem carried it as
4
+ # a second C extension until carray 3.0.2, which is why the old name is still
5
+ # here; it goes at the next minor.
6
+ #
7
+ # The file stays rather than the alias moving into `carray/jit.rb`, because
8
+ # this is what the light half loads. A program whose sites are all answered
9
+ # by a library built ahead of it never loads the compiler, and `CFunction`
10
+ # still has to be handed an address -- see `carray/jit/call`.
11
+ require "carray"
12
+ require "carray/jit/version"
13
+
14
+ CArray::JIT::Access = CArray::AddressBasis