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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +917 -3
- data/README.md +14 -12
- data/carray-jit.gemspec +1 -3
- data/docs/00_Introduction.md +5 -3
- data/docs/01_GettingStarted.md +1 -1
- data/docs/02_KernelShapes.md +94 -15
- data/docs/03_SupportedFeatures.md +665 -32
- data/docs/04_Compiling.md +34 -7
- data/docs/05_DesignNotes.md +3 -3
- data/docs/06_Cheatsheet.md +200 -7
- data/docs/07_StepByStep.ja.md +533 -0
- data/docs/07_StepByStep.md +534 -0
- data/docs/08_LanguageReference.md +427 -0
- data/examples/README.md +12 -0
- data/examples/applications/alarm.rb +121 -0
- data/examples/applications/collatz.rb +105 -0
- data/examples/applications/cubic_spline.rb +331 -0
- data/examples/applications/dithering.rb +144 -0
- data/examples/applications/group_stats.rb +115 -0
- data/examples/applications/lookup.rb +126 -0
- data/examples/applications/median_filter.rb +153 -0
- data/examples/applications/parcel_ascent.rb +220 -0
- data/examples/applications/point_in_polygon.rb +111 -0
- data/examples/applications/random_walk.rb +98 -0
- data/examples/applications/van_der_pol.rb +186 -0
- data/examples/applications/wet_bulb.rb +140 -0
- data/examples/features/10_complex.rb +14 -4
- data/examples/features/15_loops.rb +7 -1
- data/lib/carray/jit/access.rb +14 -0
- data/lib/carray/jit/analyzer.rb +2218 -140
- data/lib/carray/jit/block_reader.rb +37 -6
- data/lib/carray/jit/c_function.rb +961 -86
- data/lib/carray/jit/c_generator.rb +1970 -191
- data/lib/carray/jit/call.rb +68 -0
- data/lib/carray/jit/compiler.rb +102 -14
- data/lib/carray/jit/expression.rb +271 -33
- data/lib/carray/jit/kernel.rb +382 -32
- data/lib/carray/jit/node.rb +374 -9
- data/lib/carray/jit/sorting_networks.rb +182 -0
- data/lib/carray/jit/type_assignment.rb +424 -35
- data/lib/carray/jit/version.rb +1 -1
- data/lib/carray/jit.rb +547 -77
- metadata +23 -8
- data/ext/carray_jit_access/carray_jit_access.c +0 -460
- 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
|
data/lib/carray/jit/compiler.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
15
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 (
|
|
125
|
-
steps = Array.new(
|
|
126
|
-
step =
|
|
127
|
-
(
|
|
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 *=
|
|
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
|
|
140
|
-
|
|
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
|
-
|
|
160
|
-
#{body
|
|
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}
|
|
184
|
-
*(plan.masked ? ["uint8_t m#{i} = #{node.masked ? "k#{node.index}
|
|
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
|
-
|
|
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
|
|
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}")
|
|
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]}))"
|