carray-jit 0.1.2 → 0.1.4

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +917 -3
  3. data/README.md +14 -12
  4. data/carray-jit.gemspec +1 -3
  5. data/docs/00_Introduction.md +5 -3
  6. data/docs/01_GettingStarted.md +1 -1
  7. data/docs/02_KernelShapes.md +94 -15
  8. data/docs/03_SupportedFeatures.md +665 -32
  9. data/docs/04_Compiling.md +34 -7
  10. data/docs/05_DesignNotes.md +3 -3
  11. data/docs/06_Cheatsheet.md +200 -7
  12. data/docs/07_StepByStep.ja.md +533 -0
  13. data/docs/07_StepByStep.md +534 -0
  14. data/docs/08_LanguageReference.md +427 -0
  15. data/examples/README.md +12 -0
  16. data/examples/applications/alarm.rb +121 -0
  17. data/examples/applications/collatz.rb +105 -0
  18. data/examples/applications/cubic_spline.rb +331 -0
  19. data/examples/applications/dithering.rb +144 -0
  20. data/examples/applications/group_stats.rb +115 -0
  21. data/examples/applications/lookup.rb +126 -0
  22. data/examples/applications/median_filter.rb +153 -0
  23. data/examples/applications/parcel_ascent.rb +220 -0
  24. data/examples/applications/point_in_polygon.rb +111 -0
  25. data/examples/applications/random_walk.rb +98 -0
  26. data/examples/applications/van_der_pol.rb +186 -0
  27. data/examples/applications/wet_bulb.rb +140 -0
  28. data/examples/features/10_complex.rb +14 -4
  29. data/examples/features/15_loops.rb +7 -1
  30. data/lib/carray/jit/access.rb +14 -0
  31. data/lib/carray/jit/analyzer.rb +2218 -140
  32. data/lib/carray/jit/block_reader.rb +37 -6
  33. data/lib/carray/jit/c_function.rb +961 -86
  34. data/lib/carray/jit/c_generator.rb +1970 -191
  35. data/lib/carray/jit/call.rb +68 -0
  36. data/lib/carray/jit/compiler.rb +102 -14
  37. data/lib/carray/jit/expression.rb +271 -33
  38. data/lib/carray/jit/kernel.rb +382 -32
  39. data/lib/carray/jit/node.rb +374 -9
  40. data/lib/carray/jit/sorting_networks.rb +182 -0
  41. data/lib/carray/jit/type_assignment.rb +424 -35
  42. data/lib/carray/jit/version.rb +1 -1
  43. data/lib/carray/jit.rb +547 -77
  44. metadata +23 -8
  45. data/ext/carray_jit_access/carray_jit_access.c +0 -460
  46. data/ext/carray_jit_access/extconf.rb +0 -8
@@ -0,0 +1,68 @@
1
+ # `CArray.jit_call`, and only what defining it needs.
2
+ #
3
+ # A file that calls `jit_call` has to have the method, and having it used to
4
+ # mean loading the whole compiler -- the analyzer, the generator, the cache.
5
+ # For a program that compiles, that is what it is. For one whose call sites
6
+ # were built ahead of it by carray-jit-aot, and where a provider answers
7
+ # every one of them, it is a compiler loaded and never reached.
8
+ #
9
+ # So this is the light half: the declaration's parser, which says what a call
10
+ # takes, and the provider, which may answer without compiling. The compiler
11
+ # is required at the first site no provider answers, and not before.
12
+ #
13
+ # `require "carray/jit"` still loads everything, and a program that does not
14
+ # know about any of this sees no difference.
15
+
16
+ require "carray"
17
+ require "carray/jit/version"
18
+ require "carray/jit/errors"
19
+ require "carray/jit/c_function"
20
+
21
+ class CArray
22
+
23
+ # Compiles the block as a C function and calls it, here, with the locals
24
+ # around it:
25
+ #
26
+ # def moving_average (values, window)
27
+ # n = values.elements
28
+ # out = CArray.double(n)
29
+ # CArray.jit_call("void (*)(double *out, const double *values, " \
30
+ # "size_t n, size_t window)") {
31
+ # n.times { |i| ... }
32
+ # }
33
+ # out
34
+ # end
35
+ #
36
+ # The declaration's parameter names are the join, and they do the work
37
+ # twice over: they are the body's parameters, so the block declares none,
38
+ # and they name the locals of the method around it that the call reads.
39
+ # In C a parameter's name in a prototype is decoration; here it is the
40
+ # whole binding, and a name with no local behind it is an error at the
41
+ # call rather than a `nil` inside it.
42
+ #
43
+ # What this saves over `jit_function` plus a `call` is the argument list --
44
+ # written out, it is a second statement of what the declaration already
45
+ # said, in an order nothing checks. What it costs is reading those locals
46
+ # through the block's binding, measured at about 0.3 microseconds against a
47
+ # call that costs several.
48
+ #
49
+ # Compiled once per call site and kept, so the second call is a lookup.
50
+ # The block is read, never yielded to; a name it closes over that is not a
51
+ # parameter is refused, as it is for `jit_function`.
52
+ #
53
+ # @param prototype [String] the function's C declaration. `double (*)(...)`
54
+ # is the spelling for one with no name, which is what a call needs: the
55
+ # name would be written once to be looked at never.
56
+ # @return [Object, nil] what the function answered, or nil where it
57
+ # returns `void`.
58
+ # @raise [CArray::JIT::Unsupported] when the block falls outside the
59
+ # recognized subset, or names parameters the declaration already named.
60
+ def self.jit_call (prototype, &block)
61
+ unless block
62
+ raise JIT::Unsupported,
63
+ "`jit_call` compiles a block and calls it, and none was given"
64
+ end
65
+ JIT.call_here(prototype, block)
66
+ end
67
+
68
+ end
@@ -3,6 +3,7 @@ require "fileutils"
3
3
  require "rbconfig"
4
4
  require "tmpdir"
5
5
  require "fiddle"
6
+ require "monitor"
6
7
 
7
8
  class CArray
8
9
  module JIT
@@ -55,11 +56,32 @@ class CArray
55
56
  # only where it means something -- and Apple's linker rejects it
56
57
  # outright, which would trade a Linux crash for a macOS build that never
57
58
  # compiles at all.
59
+ #
60
+ # A compiled `jit_function` is the exception and exports no fixed name:
61
+ # its body, its helpers and the four words a window reads are static,
62
+ # and what leaves the object is the declared symbol, the shim beside it
63
+ # and the accessor -- all three carrying the body's own digest. That is
64
+ # what lets carray-jit-aot link several of them into one library.
58
65
  SYMBOLIC =
59
66
  (RbConfig::CONFIG["host_os"] =~ /darwin|mswin|mingw|cygwin/ ?
60
67
  [] : ["-Wl,-Bsymbolic"]).freeze
61
68
 
62
- FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off", *SYMBOLIC].freeze
69
+ # -fwrapv makes signed overflow wrap, as CArray's own operators do and
70
+ # as the documentation promises. Without it the compiler is free to
71
+ # assume a sum cannot overflow, so one kernel contradicts itself:
72
+ # `y = -a; y > 0 ? y : 0` stored a negative value for a = INT64_MIN.
73
+ FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off", "-fwrapv", *SYMBOLIC].freeze
74
+
75
+ # A compiled body calls a function borrowed with `jit_extern` by name,
76
+ # and the library it came from is one the process opened -- which is
77
+ # where the name is resolved, when the object is loaded. ELF leaves a
78
+ # shared object's undefined names to the loader as it is; Apple's
79
+ # linker refuses them unless told to, so a borrowed function from
80
+ # anywhere but libm (which `-lm` names) failed to link on macOS.
81
+ # `jit_extern` has opened the library before any body that calls it
82
+ # is compiled, so by the time the object loads the name is there.
83
+ LINK = (RbConfig::CONFIG["host_os"] =~ /darwin/ ?
84
+ ["-undefined", "dynamic_lookup"] : []).freeze
63
85
 
64
86
  # Kernels retained on disk. Each costs about 17 KB, so the default is
65
87
  # roughly 9 MB -- far more than any real program compiles, but a bound
@@ -141,7 +163,9 @@ class CArray
141
163
  if defined?(Gem::Specification)
142
164
  begin
143
165
  Gem::Specification.find_by_name("carray").version.to_s
144
- rescue StandardError
166
+ rescue StandardError, LoadError
167
+ # A gem that is not installed raises Gem::MissingSpecError,
168
+ # which is a LoadError rather than a StandardError.
145
169
  "none"
146
170
  end
147
171
  else
@@ -207,7 +231,21 @@ class CArray
207
231
  # kernel does not throw its object away. The file then names the
208
232
  # first site that compiled it, which is a place the code was written
209
233
  # rather than the only one.
234
+ # One compile at a time in a process. The staging path a build
235
+ # compiles to is named for the process, so two threads building at
236
+ # once wrote the same file and whichever renamed first left the other
237
+ # renaming a path that was gone -- a bare Errno::ENOENT out of three
238
+ # threads in four. Serialising them also spares the second thread the
239
+ # build: it takes the lock after the first has renamed, and finds the
240
+ # object where a later run would. Reentrant, so that a build reached
241
+ # from inside another -- which none is today -- would not stop here.
242
+ LOCK = Monitor.new
243
+
210
244
  def build (source, function_name, header: nil, flags: FLAGS)
245
+ LOCK.synchronize { build_locked(source, function_name, header: header, flags: flags) }
246
+ end
247
+
248
+ def build_locked (source, function_name, header: nil, flags: FLAGS)
211
249
  dump(header.to_s + source)
212
250
 
213
251
  directory = prepare(cache_directory)
@@ -232,13 +270,36 @@ class CArray
232
270
  # Build to a unique path and rename, so a concurrent process never
233
271
  # loads a half-written object.
234
272
  staging_path = "#{object_path}.#{Process.pid}"
235
- compile(source_path, staging_path, flags)
273
+ handle =
274
+ begin
275
+ compile(source_path, staging_path, flags)
276
+ # Opened before it is published, under the name only this
277
+ # process knows. Published first, the object stands in the
278
+ # cache for the moment between the rename and the open, and
279
+ # another process pruning in that moment can evict it -- prune
280
+ # spares what the process running it is building, which says
281
+ # nothing about what anyone else is. The build then failed to
282
+ # load what it had just compiled. Opened first, the image is
283
+ # mapped and stays mapped whatever the directory does
284
+ # afterwards, which is why evicting an object in use is safe.
285
+ load_new_object(staging_path)
286
+ rescue Exception
287
+ # A build that did not finish leaves nothing behind. The
288
+ # source is half an entry: nothing looks it up -- a hit is an
289
+ # object -- and prune globs objects too, so it would sit in the
290
+ # cache for good, one per kernel per run against a toolchain
291
+ # that cannot compile. The staging file would be swept in five
292
+ # minutes; it goes now, beside the source it came from.
293
+ remove_entry(object_path)
294
+ unlink(staging_path)
295
+ raise
296
+ end
236
297
  File.rename(staging_path, object_path)
237
298
  sweep_staging(directory)
238
299
  sweep_environments
239
300
  prune(directory, object_path)
240
301
 
241
- [load_new_object(object_path), true]
302
+ [handle, true]
242
303
  end
243
304
 
244
305
  # The flags are part of the key: the same C built two ways is two
@@ -369,13 +430,28 @@ class CArray
369
430
  # this user.
370
431
  def prepare (directory)
371
432
  FileUtils.mkdir_p(directory, :mode => 0700) unless File.directory?(directory)
372
- mode = File.stat(directory).mode
373
- if (mode & 0022) != 0
433
+ stat = File.stat(directory)
434
+ if (stat.mode & 0022) != 0
374
435
  raise CompilationError,
375
436
  "#{directory} is writable by other users; " \
376
437
  "carray-jit loads shared objects from it. " \
377
438
  "Fix its permissions or set CARRAY_JIT_CACHE elsewhere."
378
439
  end
440
+ # And whose it is, not only who may write to it. A directory of
441
+ # someone else's is one they write to whatever its mode says, and
442
+ # 0700 is what an attacker's own directory would be: a cache root
443
+ # under a shared /tmp, where this user has yet to create the
444
+ # directory for their environment, is a name someone else can take
445
+ # first -- as a directory of their own, or as a symlink to one,
446
+ # which the mode is then read through. Followed on purpose: a
447
+ # cache root symlinked to another volume is an ordinary
448
+ # arrangement, and it is the owner that says whether it is ours.
449
+ unless stat.uid == Process.euid
450
+ raise CompilationError,
451
+ "#{directory} belongs to uid #{stat.uid}, not to this user " \
452
+ "(uid #{Process.euid}); carray-jit loads shared objects " \
453
+ "from it. Set CARRAY_JIT_CACHE to a directory of your own."
454
+ end
379
455
  directory
380
456
  end
381
457
 
@@ -445,13 +521,13 @@ class CArray
445
521
 
446
522
  def remove_entry (object_path)
447
523
  source_path = object_path.sub(/#{Regexp.escape(shared_object_suffix)}\z/, ".c")
448
- [object_path, source_path].each do |path|
449
- begin
450
- File.unlink(path) if File.exist?(path)
451
- rescue StandardError
452
- nil
453
- end
454
- end
524
+ [object_path, source_path].each { |path| unlink(path) }
525
+ end
526
+
527
+ def unlink (path)
528
+ File.unlink(path) if File.exist?(path)
529
+ rescue StandardError
530
+ nil
455
531
  end
456
532
 
457
533
  def touch (path)
@@ -473,7 +549,19 @@ class CArray
473
549
  end
474
550
 
475
551
  def compile (source_path, object_path, flags = FLAGS)
476
- command = [compiler_command, *flags, source_path, "-o", object_path, "-lm"]
552
+ # Asked before the spawn, because a spawn that finds nothing raises
553
+ # Errno::ENOENT naming the program and nothing else -- not what it
554
+ # was wanted for, and not where to say so. This is the same lookup
555
+ # the key already makes to identify the compiler.
556
+ unless resolve_compiler(compiler_command)
557
+ raise CompilationError,
558
+ "`#{compiler_command}` is not an executable this process " \
559
+ "can find; carray-jit compiles its kernels with it. " \
560
+ "Set CARRAY_JIT_CC to a C compiler, or leave it unset for " \
561
+ "the one Ruby was built with."
562
+ end
563
+ command = [compiler_command, *flags, source_path, "-o", object_path,
564
+ "-lm", *LINK]
477
565
  output = IO.popen(command, err: [:child, :out]) { |io| io.read }
478
566
  unless $?.success?
479
567
  raise CompilationError, "#{command.join(' ')}\n#{output}"
@@ -11,8 +11,15 @@ class CArray
11
11
  # nothing here restates any of that: it substitutes operands into bodies
12
12
  # it was given and wraps the result in a loop.
13
13
  #
14
- # Declining is ordinary. An expression this cannot address -- an operand
15
- # that is not laid out end to end, a data type with no C to write it in --
14
+ # An operand need not be laid out end to end. A slice, a transpose or a
15
+ # broadcast column is read where it lies, by the byte strides CArray
16
+ # lends for it -- a broadcast axis is a stride of 0 -- and a view with no
17
+ # strides of its own is lent as a packed copy of it. The strides are
18
+ # handed over at the call, so one kernel serves every view of the same
19
+ # kind; which operands are read that way is part of what it is cached by.
20
+ #
21
+ # Declining is ordinary. An expression this cannot address -- a data
22
+ # type with no C to write it in, a destination whose cells overlap --
16
23
  # goes back to CArray, which walks it and arrives at the same answer.
17
24
  class Expression
18
25
 
@@ -54,14 +61,22 @@ class CArray
54
61
  def call (plan, out)
55
62
  return false unless C_TYPES.key?(plan.data_type)
56
63
  aliased = plan.leaves.any? { |array| array.equal?(out) }
57
- kernel = kernel_for(plan, aliased) or return false
64
+ # A shifted read reaches cells other than the one being written, so
65
+ # over its own output it would read what it has just written.
66
+ return false if aliased && shifted_leaves(plan).any? { |i| plan.leaves[i].equal?(out) }
67
+ return false unless out.dim == plan.dim
58
68
  arrays = [out, *plan.leaves]
59
69
  writable = [true, *Array.new(plan.leaves.size, false)]
60
70
  Access.open(arrays, writable) do |bases|
61
- return false unless bases.each_with_index.all? { |basis, i|
62
- basis[:strides] == end_to_end(arrays[i])
63
- }
64
- kernel.call(out.elements, *pointers(plan, bases))
71
+ strided = strided_slots(arrays, bases) or return false
72
+ kernel = kernel_for(plan, aliased, strided) or return false
73
+ # Held here so that the buffers outlive the call.
74
+ dim = out.dim.pack("q*")
75
+ steps = strided.flat_map { |slot| slot_strides(bases, slot) }.pack("q*")
76
+ args = [out.elements]
77
+ args << Fiddle::Pointer[dim] if walks_axes?(plan, strided)
78
+ args << Fiddle::Pointer[steps] unless strided.empty?
79
+ kernel.call(*args, *pointers(plan, bases))
65
80
  end
66
81
  true
67
82
  rescue ZeroDivisionError
@@ -88,27 +103,46 @@ class CArray
88
103
  private
89
104
 
90
105
  # An expression of the same shape compiles to the same kernel whatever
91
- # arrays it is over, which is what the plan's signature says.
92
- def kernel_for (plan, aliased)
93
- @kernels.fetch([plan.signature, aliased]) do
94
- @kernels[[plan.signature, aliased]] = compile(plan, aliased)
106
+ # arrays it is over, which is what the plan's signature says -- given
107
+ # the same operands read by strides. The strides themselves are
108
+ # arguments, not part of the kernel.
109
+ def kernel_for (plan, aliased, strided)
110
+ key = [plan.signature, aliased, strided]
111
+ @kernels.fetch(key) do
112
+ @kernels[key] = compile(plan, aliased, strided)
95
113
  end
96
114
  end
97
115
 
98
- def compile (plan, aliased)
116
+ def compile (plan, aliased, strided)
117
+ @strided = strided
99
118
  source = source_for(plan, aliased) or return nil
100
119
  handle, = Compiler.build(source, "carray_jit_expression", flags: FLAGS)
101
120
  Fiddle::Function.new(handle["carray_jit_expression"],
102
121
  [Fiddle::TYPE_LONG_LONG] +
103
- [Fiddle::TYPE_VOIDP] * (1 + arity(plan)),
122
+ [Fiddle::TYPE_VOIDP] * (1 + arity(plan, strided)),
104
123
  Fiddle::TYPE_VOID)
105
124
  rescue CompilationError
106
125
  nil
107
126
  end
108
127
 
109
- def arity (plan)
128
+ def arity (plan, strided)
110
129
  plan.leaves.size + plan.leaves.count { |array| array.has_mask? } +
111
- (plan.masked ? 1 : 0)
130
+ (plan.masked ? 1 : 0) + (walks_axes?(plan, strided) ? 1 : 0) +
131
+ (strided.empty? ? 0 : 1)
132
+ end
133
+
134
+ # A plan that reads an operand shifted, or by strides, needs to know
135
+ # where along each axis the cell is; any other is one flat loop.
136
+ def walks_axes? (plan, strided)
137
+ shifted?(plan) || ! strided.empty?
138
+ end
139
+
140
+ def shifted? (plan)
141
+ plan.nodes.any? { |node| node.is_a?(CArray::Fusion::Shifted) }
142
+ end
143
+
144
+ def shifted_leaves (plan)
145
+ plan.nodes.grep(CArray::Fusion::Shifted).map(&:index)
112
146
  end
113
147
 
114
148
  def pointers (plan, bases)
@@ -121,23 +155,80 @@ class CArray
121
155
  args
122
156
  end
123
157
 
124
- def end_to_end (array)
125
- steps = Array.new(array.ndim)
126
- step = array.bytes
127
- (array.ndim - 1).downto(0) do |axis|
158
+ def end_to_end (dim, bytes)
159
+ steps = Array.new(dim.size)
160
+ step = bytes
161
+ (dim.size - 1).downto(0) do |axis|
128
162
  steps[axis] = step
129
- step *= array.dim[axis]
163
+ step *= dim[axis]
130
164
  end
131
165
  steps
132
166
  end
133
167
 
168
+ # The C names of the pointers that are not laid out end to end, in the
169
+ # order their strides are handed over: the destination, its mask, then
170
+ # each operand and its mask. Nil where the destination has two cells
171
+ # at one address -- writing them one at a time is not what the walk
172
+ # does -- or where an operand is not the shape of the destination.
173
+ def strided_slots (arrays, bases)
174
+ slots = []
175
+ bases.each_with_index do |basis, i|
176
+ return nil unless basis[:dim] == arrays[0].dim
177
+ data, mask = i == 0 ? ["out", "out_mask"] : ["a#{i - 1}", "k#{i - 1}"]
178
+ if i == 0
179
+ return nil unless one_to_one?(basis[:strides], basis[:dim], basis[:bytes])
180
+ return nil if basis[:mask_strides] && ! one_to_one?(basis[:mask_strides], basis[:dim], 1)
181
+ end
182
+ slots << data unless basis[:strides] == end_to_end(basis[:dim], basis[:bytes])
183
+ if basis[:mask_strides] && basis[:mask_strides] != end_to_end(basis[:dim], 1)
184
+ slots << mask
185
+ end
186
+ end
187
+ slots.freeze
188
+ end
189
+
190
+ def slot_strides (bases, slot)
191
+ index = slot.start_with?("out") ? 0 : slot[1..].to_i + 1
192
+ slot.start_with?("k", "out_mask") ? bases[index][:mask_strides] : bases[index][:strides]
193
+ end
194
+
195
+ # Whether every cell has an address of its own. Taken axis by axis
196
+ # from the shortest stride up, each has to step past everything the
197
+ # ones below it reach. A layout that interleaves axes without sharing
198
+ # an address fails this too, and is declined though it need not be.
199
+ def one_to_one? (strides, dim, bytes)
200
+ reach = bytes
201
+ strides.zip(dim).reject { |_, d| d == 1 }.sort_by { |s, _| s.abs }.all? do |s, d|
202
+ fits = s.abs >= reach
203
+ reach += s.abs * (d - 1)
204
+ fits
205
+ end
206
+ end
207
+
208
+ # A cell of the array `slot` names: at the flat index `flat` where it
209
+ # is laid out end to end, and at the sum of `axes` times its strides
210
+ # where it is not.
211
+ def cell (slot, type, flat, axes)
212
+ return "#{slot}[#{flat}]" unless @strided.include?(slot)
213
+ offset = axes.each_with_index.map { |a, k| "(#{a}) * s_#{slot}_#{k}" }.join(" + ")
214
+ qualifier = slot.start_with?("out") ? "" : "const "
215
+ "*(#{qualifier}#{type} *) ((#{qualifier}char *) #{slot} + #{offset})"
216
+ end
217
+
218
+ def axes (plan)
219
+ (0...plan.dim.size).map { |k| "i#{k}" }
220
+ end
221
+
134
222
  # -- the C ------------------------------------------------------------
135
223
 
136
224
  def source_for (plan, aliased)
137
225
  out_type = C_TYPES.fetch(plan.data_type)
138
226
  restrict = aliased ? "" : "restrict "
139
- body = plan.nodes.each_with_index.map { |node, i| line(plan, node, i) }
140
- return nil if body.any?(&:nil?)
227
+ body = statements(plan, :edge) or return nil
228
+ last = plan.nodes.size - 1
229
+ result = ["#{cell("out", out_type, "n", axes(plan))} = v#{last};",
230
+ *(plan.masked ? ["#{cell("out_mask", "uint8_t", "n", axes(plan))} = m#{last};"] : [])]
231
+ walking = walks_axes?(plan, @strided)
141
232
  <<~C
142
233
  #include <math.h>
143
234
  #include <stdlib.h>
@@ -153,17 +244,92 @@ class CArray
153
244
  typedef float float32_t;
154
245
  typedef double float64_t;
155
246
 
247
+ #{helpers}
156
248
  void
157
- carray_jit_expression (int64_t elements, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
249
+ carray_jit_expression (int64_t elements#{walking ? ", const int64_t *dim" : ""}#{@strided.empty? ? "" : ", const int64_t *steps"}, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
158
250
  {
159
- for ( int64_t n = 0; n < elements; n++ ) {
160
- #{body.flatten.map { |l| " " + l }.join("\n")}
161
- out[n] = v#{plan.nodes.size - 1};#{plan.masked ? "\n out_mask[n] = m#{plan.nodes.size - 1};" : ""}
162
- }
251
+ #{stride_lines(plan).join("\n")}
252
+ #{loop(plan, body + result, walking && (statements(plan, :inside) + result)).join("\n")}
163
253
  }
164
254
  C
165
255
  end
166
256
 
257
+ # The C for every node, in order. `where` matters only to a shifted
258
+ # read: at the :edge it may fall outside the array, :inside it cannot.
259
+ def statements (plan, where)
260
+ @where = where
261
+ body = plan.nodes.each_with_index.map { |node, i| line(plan, node, i) }
262
+ body.any?(&:nil?) ? nil : body.flatten
263
+ end
264
+
265
+ # The strides handed over, one per axis for each slot, named.
266
+ def stride_lines (plan)
267
+ ndim = plan.dim.size
268
+ @strided.each_with_index.flat_map { |slot, j|
269
+ (0...ndim).map { |k| " const int64_t s_#{slot}_#{k} = steps[#{j * ndim + k}];" }
270
+ }
271
+ end
272
+
273
+ # Cell by cell in storage order. A plan that reads an array shifted
274
+ # or by strides needs to know where along each axis the cell is, so it
275
+ # walks the outer axes and then the last. A shifted read splits the
276
+ # last one in three: the stretch in the middle, where no shifted read
277
+ # can leave the array, is a plain loop the compiler can vectorise, and
278
+ # only the ends check.
279
+ def loop (plan, edge, inside = nil)
280
+ unless inside
281
+ return [" for ( int64_t n = 0; n < elements; n++ ) {",
282
+ *edge.map { |l| " " + l }, " }"]
283
+ end
284
+ ndim = plan.dim.size
285
+ last = ndim - 1
286
+ offsets = plan.nodes.grep(CArray::Fusion::Shifted).map(&:offset)
287
+ lines = (0...ndim).map { |k| " const int64_t d#{k} = dim[#{k}];" }
288
+ plan.nodes.each_with_index do |node, i|
289
+ next unless node.is_a?(CArray::Fusion::Shifted)
290
+ flat = (1...ndim).reduce("(int64_t)(#{node.offset[0]})") { |acc, k|
291
+ "(#{acc}) * d#{k} + (#{node.offset[k]})"
292
+ }
293
+ lines << " const int64_t o#{i} = #{flat};"
294
+ end
295
+ (0...ndim).each do |k|
296
+ break if offsets.empty?
297
+ below = [0, *offsets.map { |o| -o[k] }].max
298
+ above = [0, *offsets.map { |o| o[k] }].max
299
+ lines << " const int64_t lo#{k} = d#{k} < #{below} ? d#{k} : #{below};"
300
+ lines << " const int64_t hi#{k} = d#{k} - #{above} > lo#{k} ? d#{k} - #{above} : lo#{k};"
301
+ end
302
+ indent = " "
303
+ (0...last).each do |k|
304
+ lines << "#{indent}for ( int64_t i#{k} = 0; i#{k} < d#{k}; i#{k}++ ) {"
305
+ indent += " "
306
+ end
307
+ row = (0...last).reduce("(int64_t) 0") { |acc, k| "(#{acc}) * d#{k} + i#{k}" }
308
+ row = "(#{row}) * d#{last}"
309
+ outer = (0...last).map { |k| "i#{k} >= lo#{k} && i#{k} < hi#{k}" }
310
+ lines << "#{indent}const int64_t row = #{row};"
311
+ unless offsets.empty?
312
+ lines << "#{indent}const int64_t a = #{outer.empty? ? "lo#{last}" : "(#{outer.join(" && ")}) ? lo#{last} : d#{last}"};"
313
+ lines << "#{indent}const int64_t b = a < hi#{last} ? hi#{last} : a;"
314
+ end
315
+ stretches = if offsets.empty?
316
+ [["0", "d#{last}", edge]]
317
+ else
318
+ [["0", "a", edge], ["a", "b", inside], ["b", "d#{last}", edge]]
319
+ end
320
+ stretches.each do |from, to, body|
321
+ lines << "#{indent}for ( int64_t i#{last} = #{from}; i#{last} < #{to}; i#{last}++ ) {"
322
+ lines << "#{indent} const int64_t n = row + i#{last};"
323
+ lines.concat(body.map { |l| "#{indent} " + l })
324
+ lines << "#{indent}}"
325
+ end
326
+ (last - 1).downto(0) do |k|
327
+ indent = indent[2..]
328
+ lines << "#{indent}}"
329
+ end
330
+ lines
331
+ end
332
+
167
333
  def mask_parameter (plan, restrict)
168
334
  plan.masked ? ", uint8_t *#{restrict}out_mask" : ""
169
335
  end
@@ -180,31 +346,100 @@ class CArray
180
346
  type = C_TYPES[node.data_type] or return nil
181
347
  case node
182
348
  when CArray::Fusion::Leaf
183
- ["#{type} v#{i} = a#{node.index}[n];",
184
- *(plan.masked ? ["uint8_t m#{i} = #{node.masked ? "k#{node.index}[n]" : "0"};"] : [])]
349
+ ["#{type} v#{i} = #{cell("a#{node.index}", type, "n", axes(plan))};",
350
+ *(plan.masked ? ["uint8_t m#{i} = #{node.masked ? cell("k#{node.index}", "uint8_t", "n", axes(plan)) : "0"};"] : [])]
351
+ when CArray::Fusion::Shifted
352
+ shifted_line(plan, node, i, type)
185
353
  when CArray::Fusion::Const
186
354
  ["#{type} v#{i} = #{literal(node)};",
187
355
  *(plan.masked ? ["uint8_t m#{i} = 0;"] : [])]
188
356
  when CArray::Fusion::Op
189
357
  statement = substitute(node, i, type) or return nil
190
- [*(plan.masked ? [mask_line(node, i)] : []),
358
+ # A mask rule this gem does not know is a plan it declines.
359
+ mask = plan.masked ? (mask_line(node, i) or return nil) : nil
360
+ [*mask,
191
361
  "#{type} v#{i};",
192
362
  *guarded(node, i, statement, plan.masked)]
193
363
  end
194
364
  end
195
365
 
366
+ # The cell `offset` away, read from an index clamped into the array so
367
+ # that the load is always in bounds, then replaced where it fell out.
368
+ def shifted_line (plan, node, i, type)
369
+ bounds = node.bounds.uniq
370
+ return nil unless bounds.size == 1
371
+ ndim = node.offset.size
372
+ if @where == :inside
373
+ moved = (0...ndim).map { |k| "i#{k} + (#{node.offset[k]})" }
374
+ return ["#{type} v#{i} = #{cell("a#{node.index}", type, "n + o#{i}", moved)};",
375
+ *(plan.masked ? ["uint8_t m#{i} = #{node.masked ? cell("k#{node.index}", "uint8_t", "n + o#{i}", moved) : "0"};"] : [])]
376
+ end
377
+ index = (0...ndim).map { |k| "s#{i}_#{k}" }
378
+ lines = node.offset.each_with_index.map { |o, k|
379
+ "int64_t #{index[k]} = i#{k} + (#{o});"
380
+ }
381
+ inside = (0...ndim).map { |k| "#{index[k]} >= 0 && #{index[k]} < d#{k}" }.join(" && ")
382
+ lines << "int s#{i}_in = #{inside};"
383
+ (0...ndim).each { |k|
384
+ lines << "int64_t c#{i}_#{k} = #{index[k]} < 0 ? 0 : (#{index[k]} >= d#{k} ? d#{k} - 1 : #{index[k]});"
385
+ }
386
+ flat = (1...ndim).reduce("c#{i}_0") { |acc, k| "(#{acc}) * d#{k} + c#{i}_#{k}" }
387
+ clamped = (0...ndim).map { |k| "c#{i}_#{k}" }
388
+ lines << "#{type} v#{i} = #{cell("a#{node.index}", type, flat, clamped)};"
389
+ if bounds.first == :fill
390
+ fill = CArray::Fusion::Const.new(node.fill, node.data_type)
391
+ lines << "if ( ! s#{i}_in ) { v#{i} = #{literal(fill)}; }"
392
+ end
393
+ if plan.masked
394
+ inner = node.masked ? cell("k#{node.index}", "uint8_t", flat, clamped) : "0"
395
+ outer = bounds.first == :mask ? "1" : "0"
396
+ lines << "uint8_t m#{i} = s#{i}_in ? #{inner} : #{outer};"
397
+ end
398
+ lines
399
+ end
400
+
196
401
  def literal (node)
197
402
  case node.data_type
198
- when :float64, :float32 then "%.17g" % node.value
199
- when :boolean then node.value ? 1 : 0
403
+ when :float64, :float32 then float_literal(node.value.to_f)
404
+ when :boolean then truth(node.value) ? 1 : 0
200
405
  else node.value.to_s
201
406
  end
202
407
  end
203
408
 
409
+ # `%.17g` writes -0.0 as "-0", which C reads as the integer 0 negated,
410
+ # and writes NaN and the infinities as words C does not know.
411
+ def float_literal (value)
412
+ return "NAN" if value.nan?
413
+ return (value > 0 ? "INFINITY" : "(-INFINITY)") if value.infinite?
414
+ text = "%.17g" % value
415
+ text.match?(/[.e]/) ? text : "#{text}.0"
416
+ end
417
+
418
+ # A boolean cell as CArray stores it: 0 is false, as is false.
419
+ def truth (value)
420
+ value.is_a?(Numeric) ? value != 0 : !!value
421
+ end
422
+
423
+ # `<type>` names a helper by CArray's spelling of the type -- the
424
+ # float64_t of op_powi_float64_t -- not by its C spelling.
204
425
  def substitute (node, i, type)
205
426
  text = node.body.dup
206
427
  node.args.each_with_index { |arg, k| text = text.gsub("##{k + 1}", "v#{arg}") }
207
- text.gsub("##{node.args.size + 1}", "v#{i}").gsub("<type>", type).lines.map(&:strip)
428
+ text.gsub("##{node.args.size + 1}", "v#{i}")
429
+ .gsub("<type>", carray_type(node.data_type, type)).lines.map(&:strip)
430
+ end
431
+
432
+ def carray_type (data_type, c_type)
433
+ case data_type
434
+ when :float32, :float64 then "#{data_type}_t"
435
+ else c_type
436
+ end
437
+ end
438
+
439
+ # The C a body may call beyond the standard library, as CArray hands it
440
+ # over.
441
+ def helpers
442
+ CArray.__kernel_helpers__
208
443
  end
209
444
 
210
445
  # A masked cell is not computed where computing it would raise: the
@@ -220,6 +455,9 @@ class CArray
220
455
  case node.mask
221
456
  when :pass then "uint8_t m#{i} = m#{args[0]};"
222
457
  when :union then "uint8_t m#{i} = #{args.map { |a| "m#{a}" }.join(" | ")};"
458
+ # The condition's mask, else the mask of the operand it chooses.
459
+ when :select
460
+ "uint8_t m#{i} = m#{args[0]} | (v#{args[0]} ? m#{args[1]} : m#{args[2]});"
223
461
  when :kleene_or, :kleene_and
224
462
  known = if node.mask == :kleene_or
225
463
  "((!m#{args[0]} && v#{args[0]}) || (!m#{args[1]} && v#{args[1]}))"