carray-jit 0.1.0
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 +7 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +84 -0
- data/LICENSE +21 -0
- data/README.md +88 -0
- data/bin/carray-jit +194 -0
- data/carray-jit.gemspec +41 -0
- data/docs/00_Introduction.md +40 -0
- data/docs/01_GettingStarted.md +80 -0
- data/docs/02_KernelShapes.md +397 -0
- data/docs/03_SupportedFeatures.md +595 -0
- data/docs/04_Compiling.md +234 -0
- data/docs/05_DesignNotes.md +136 -0
- data/docs/06_Cheatsheet.md +177 -0
- data/examples/README.md +56 -0
- data/examples/applications/game_of_life.rb +161 -0
- data/examples/applications/heat_equation.rb +117 -0
- data/examples/applications/kepler.rb +178 -0
- data/examples/applications/mandelbrot.rb +151 -0
- data/examples/applications/moving_average.rb +124 -0
- data/examples/applications/partial_sums.rb +141 -0
- data/examples/applications/point_cloud.rb +110 -0
- data/examples/applications/quicksort.rb +118 -0
- data/examples/applications/recursion.rb +121 -0
- data/examples/applications/relaxation.rb +115 -0
- data/examples/applications/sensor_gaps.rb +118 -0
- data/examples/applications/sieve.rb +95 -0
- data/examples/applications/sobel_edges.rb +80 -0
- data/examples/features/01_element_wise.rb +69 -0
- data/examples/features/02_stencil.rb +40 -0
- data/examples/features/03_recurrence.rb +50 -0
- data/examples/features/04_thomas.rb +81 -0
- data/examples/features/05_reduction.rb +90 -0
- data/examples/features/06_jit_contract.rb +58 -0
- data/examples/features/07_masks.rb +55 -0
- data/examples/features/08_views.rb +46 -0
- data/examples/features/09_inspecting.rb +55 -0
- data/examples/features/10_complex.rb +107 -0
- data/examples/features/11_c_functions.rb +260 -0
- data/examples/features/12_sweep.rb +139 -0
- data/examples/features/13_cscalar.rb +80 -0
- data/examples/features/14_stencil_window.rb +106 -0
- data/examples/features/15_loops.rb +148 -0
- data/examples/features/16_raising.rb +69 -0
- data/ext/carray_jit_access/carray_jit_access.c +460 -0
- data/ext/carray_jit_access/extconf.rb +8 -0
- data/lib/carray/jit/analyzer.rb +1847 -0
- data/lib/carray/jit/block_reader.rb +139 -0
- data/lib/carray/jit/c_function.rb +777 -0
- data/lib/carray/jit/c_generator.rb +2305 -0
- data/lib/carray/jit/compiler.rb +468 -0
- data/lib/carray/jit/errors.rb +37 -0
- data/lib/carray/jit/expression.rb +202 -0
- data/lib/carray/jit/kernel.rb +509 -0
- data/lib/carray/jit/node.rb +573 -0
- data/lib/carray/jit/sweep.rb +97 -0
- data/lib/carray/jit/type_assignment.rb +811 -0
- data/lib/carray/jit/version.rb +5 -0
- data/lib/carray/jit.rb +1210 -0
- metadata +139 -0
data/lib/carray/jit.rb
ADDED
|
@@ -0,0 +1,1210 @@
|
|
|
1
|
+
require "carray"
|
|
2
|
+
|
|
3
|
+
require "carray/jit/version"
|
|
4
|
+
require "carray/jit/errors"
|
|
5
|
+
require "carray/jit/node"
|
|
6
|
+
require "carray/jit/analyzer"
|
|
7
|
+
require "carray/jit/block_reader"
|
|
8
|
+
require "carray/jit/c_function"
|
|
9
|
+
require "carray/jit/type_assignment"
|
|
10
|
+
require "carray/jit/c_generator"
|
|
11
|
+
require "carray/jit/compiler"
|
|
12
|
+
require "carray/jit/access"
|
|
13
|
+
require "carray/jit/kernel"
|
|
14
|
+
|
|
15
|
+
class CArray
|
|
16
|
+
|
|
17
|
+
# @!group Compiling a block
|
|
18
|
+
|
|
19
|
+
# Returns the kernel compiled from `block` and run over `extents`.
|
|
20
|
+
#
|
|
21
|
+
# The block's parameters are the loop indices, and each extent is a Range or
|
|
22
|
+
# an Integer standing for `0...n`, one per index:
|
|
23
|
+
#
|
|
24
|
+
# CArray.jit_for(2...24) { |i|
|
|
25
|
+
# w = x * leg[i-1]
|
|
26
|
+
# wy = w - leg[i-2]
|
|
27
|
+
# leg[i] = wy + w - wy/i
|
|
28
|
+
# }
|
|
29
|
+
#
|
|
30
|
+
# Naming an index is what lets a kernel reach a neighbouring cell, and
|
|
31
|
+
# reaching a neighbouring cell is what makes the range and the direction
|
|
32
|
+
# matter. A computation that reaches no neighbour names no index and needs
|
|
33
|
+
# no extent, and is written with CArray.jit_each or CArray.jit_map instead.
|
|
34
|
+
#
|
|
35
|
+
# Arrays and scalars are the variables the block closes over, so nothing has
|
|
36
|
+
# to be named twice. Which way each axis runs is derived from the kernel's
|
|
37
|
+
# own dependencies, not chosen: reading a cell the kernel will later write
|
|
38
|
+
# means that cell has to be reached in one particular order.
|
|
39
|
+
#
|
|
40
|
+
# A block outside the compilable subset raises CArray::JIT::Unsupported
|
|
41
|
+
# rather than falling back to a Ruby loop. Nobody calls this method except
|
|
42
|
+
# to make a per-cell computation fast, so quietly doing the slow thing would
|
|
43
|
+
# answer a question that was not asked.
|
|
44
|
+
#
|
|
45
|
+
# The name is CArray's own: carray/lazy.rb defines jit_for to raise
|
|
46
|
+
# NotImplementedError, saying that the block is compiled and that the
|
|
47
|
+
# compiler is this gem. Requiring carray/jit replaces it with this one.
|
|
48
|
+
# So the method is named where the subset is documented, and a program that
|
|
49
|
+
# calls it either compiles or is told why it cannot -- an expression over
|
|
50
|
+
# whole arrays that needs no compiler is CArray.fuse's.
|
|
51
|
+
#
|
|
52
|
+
# `reassociate:` says whether a reduction's accumulator may be split into
|
|
53
|
+
# partial sums. It defaults to CArray::JIT.reassociate, which is true:
|
|
54
|
+
# the answer is then not the one a serial Ruby loop reaches, and is usually
|
|
55
|
+
# the more accurate of the two, because splitting the accumulation is what
|
|
56
|
+
# limits the cancellation. Pass false for the serial order -- for a
|
|
57
|
+
# compensated summation, whose algorithm *is* the order, or to check a
|
|
58
|
+
# kernel against the loop it replaces.
|
|
59
|
+
#
|
|
60
|
+
# Returns the compiled kernel, whose {CArray::JIT::CompiledKernel#c_source} is the
|
|
61
|
+
# C that ran.
|
|
62
|
+
#
|
|
63
|
+
# The block is read and compiled, never called, so it is not yielded to.
|
|
64
|
+
#
|
|
65
|
+
# @param extents [Array<Range, Integer>] one per loop index, an Integer
|
|
66
|
+
# standing for `0...n`.
|
|
67
|
+
# @param reassociate [Boolean, nil] whether a reduction's accumulator may be
|
|
68
|
+
# split into partial sums; `nil` defers to {CArray::JIT.reassociate}.
|
|
69
|
+
# @return [CArray::JIT::CompiledKernel]
|
|
70
|
+
# @raise [CArray::JIT::Unsupported] when the block falls outside the
|
|
71
|
+
# recognized subset, or the extents do not match its parameters.
|
|
72
|
+
def self.jit_for (*extents, reassociate: nil, &block)
|
|
73
|
+
unless block
|
|
74
|
+
raise JIT::Unsupported, "jit_for needs a block"
|
|
75
|
+
end
|
|
76
|
+
if block.arity.zero?
|
|
77
|
+
raise JIT::Unsupported,
|
|
78
|
+
"jit_for's block names the cells it is on, so it takes the loop " \
|
|
79
|
+
"indices as its parameters; a block that names none is " \
|
|
80
|
+
"element-wise and belongs to jit_each"
|
|
81
|
+
end
|
|
82
|
+
JIT.run(extents, block, reassociate)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns the kernel compiled from `block` and run at every cell.
|
|
86
|
+
#
|
|
87
|
+
# Element-wise means what it means in CArray: every cell is computed from
|
|
88
|
+
# the cells beside it in the other arrays, none reaches a neighbour, and the
|
|
89
|
+
# shapes are broadcast. So there is no index to name and no extent to give,
|
|
90
|
+
# and what changes is not the expression but how it runs -- at the cell, in
|
|
91
|
+
# one pass, instead of one pass per operation with intermediate arrays in
|
|
92
|
+
# between.
|
|
93
|
+
#
|
|
94
|
+
# CArray.jit_each { out = a + b * c }
|
|
95
|
+
#
|
|
96
|
+
# The arrays are the variables the block closes over, and the expression is
|
|
97
|
+
# written the way CArray already writes it. Every name in the block is a
|
|
98
|
+
# cell -- the loop is this compiler's and is not written here -- so the
|
|
99
|
+
# assignment is Ruby's own: `out = ...` writes the cell of the array `out`
|
|
100
|
+
# names outside. A name that is not an array there is a local of the
|
|
101
|
+
# block's, as it is anywhere else.
|
|
102
|
+
#
|
|
103
|
+
# `each` is what CArray means by it: a cell. It is jit_for's sibling and
|
|
104
|
+
# not its special case -- a block that names an index is on a cell and can
|
|
105
|
+
# reach the cells around it, which is what makes a range and a direction
|
|
106
|
+
# matter, while an element-wise block has neither. And it is jit_map's
|
|
107
|
+
# sibling in the other direction: the name says whether a value comes back.
|
|
108
|
+
#
|
|
109
|
+
# Like jit_for, the name is CArray's own and raises there until this gem
|
|
110
|
+
# replaces it. Written without a compiler, the same computation is
|
|
111
|
+
# CArray.fuse's -- the expression itself, one pass per operation, with the
|
|
112
|
+
# intermediates this one does without.
|
|
113
|
+
#
|
|
114
|
+
# Returns the compiled kernel, whose #c_source is the C that ran. The
|
|
115
|
+
# value of the computation is in the
|
|
116
|
+
# arrays the block wrote; ask for it back with {CArray.jit_map} instead.
|
|
117
|
+
#
|
|
118
|
+
# The block is read and compiled, never called, so it is not yielded to.
|
|
119
|
+
#
|
|
120
|
+
# @return [CArray::JIT::CompiledKernel]
|
|
121
|
+
# @raise [CArray::JIT::Unsupported] when the block falls outside the
|
|
122
|
+
# recognized subset, or names any parameter.
|
|
123
|
+
def self.jit_each (&block)
|
|
124
|
+
unless block
|
|
125
|
+
raise JIT::Unsupported, "jit_each needs a block"
|
|
126
|
+
end
|
|
127
|
+
unless block.arity.zero?
|
|
128
|
+
raise JIT::Unsupported,
|
|
129
|
+
"this block names the arrays it reaches, so it takes no " \
|
|
130
|
+
"parameters; for a loop that names its indices, see `jit_for`"
|
|
131
|
+
end
|
|
132
|
+
JIT.run_over_whole_arrays(block)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Returns a new array holding `block`'s value at every cell.
|
|
136
|
+
#
|
|
137
|
+
# The same block as {CArray.jit_each}, with its value asked for:
|
|
138
|
+
#
|
|
139
|
+
# larger = CArray.jit_map { a > b ? a : b }
|
|
140
|
+
#
|
|
141
|
+
# The last statement is the value every cell of the result gets, and the
|
|
142
|
+
# result is allocated here and returned, typed from that value. An
|
|
143
|
+
# assignment may be the last statement -- in Ruby an assignment has the
|
|
144
|
+
# value it assigned -- so a block may write an array of yours and hand the
|
|
145
|
+
# same value back.
|
|
146
|
+
#
|
|
147
|
+
# The block is read and compiled, never called, so it is not yielded to.
|
|
148
|
+
#
|
|
149
|
+
# @return [CArray] a new array, typed from the block's value.
|
|
150
|
+
# @raise [CArray::JIT::Unsupported] when the block falls outside the
|
|
151
|
+
# recognized subset, or names any parameter.
|
|
152
|
+
def self.jit_map (&block)
|
|
153
|
+
unless block
|
|
154
|
+
raise JIT::Unsupported, "jit_map needs a block"
|
|
155
|
+
end
|
|
156
|
+
unless block.arity.zero?
|
|
157
|
+
raise JIT::Unsupported,
|
|
158
|
+
"this block names the arrays it reaches, so it takes no " \
|
|
159
|
+
"parameters; for a loop that names its indices, see `jit_for`"
|
|
160
|
+
end
|
|
161
|
+
JIT.run_over_whole_arrays(block, map: true)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Returns a new array holding `block`'s value at every cell, computed from
|
|
165
|
+
# windows onto `arrays`.
|
|
166
|
+
#
|
|
167
|
+
# A stencil: every cell from the ones around it, with the loop implied.
|
|
168
|
+
#
|
|
169
|
+
# smoothed = CArray.jit_stencil(image) { |a|
|
|
170
|
+
# 0.25 * (a[-1, 0] + a[1, 0] + a[0, -1] + a[0, 1])
|
|
171
|
+
# }
|
|
172
|
+
#
|
|
173
|
+
# The arrays are given rather than closed over, and the block's parameters
|
|
174
|
+
# are windows onto them, in that order: `a[0, 0]` is the cell, `a[-1, 1]`
|
|
175
|
+
# its neighbour. The block's value is what the cell gets, as jit_map's is,
|
|
176
|
+
# and the result comes back -- an array of the same shape.
|
|
177
|
+
#
|
|
178
|
+
# Naming a window rather than an index is what lets the edge be said at the
|
|
179
|
+
# call rather than written into the loop. Where the window falls off the
|
|
180
|
+
# array there is nothing to read, and `border:` says what to do about it:
|
|
181
|
+
#
|
|
182
|
+
# :mask the cell is UNDEF -- it was not computed (the default)
|
|
183
|
+
# :skip the cell is left as it was found
|
|
184
|
+
#
|
|
185
|
+
# The default is `:mask` because CArray can say "not computed", and a
|
|
186
|
+
# border of zeros that means the same thing cannot be told from zeros that
|
|
187
|
+
# were computed.
|
|
188
|
+
#
|
|
189
|
+
# `type:` names the array to collect into; without it the type is the
|
|
190
|
+
# block's value's, as jit_map's result is. `into:` writes into an array of
|
|
191
|
+
# yours instead, and then the type is that array's.
|
|
192
|
+
#
|
|
193
|
+
# The block is read and compiled, never called, so it is not yielded to.
|
|
194
|
+
#
|
|
195
|
+
# @param arrays [Array<CArray>] the arrays the block has windows onto, in
|
|
196
|
+
# the order its parameters name them.
|
|
197
|
+
# @param border [Symbol] `:mask` to leave an uncomputed cell UNDEF,
|
|
198
|
+
# `:skip` to leave it as it was found.
|
|
199
|
+
# @param type [Symbol, nil] the data type to collect into; `nil` takes the
|
|
200
|
+
# block's value's.
|
|
201
|
+
# @param into [CArray, nil] an array of yours to write into, which then
|
|
202
|
+
# decides the type.
|
|
203
|
+
# @return [CArray] `into` when it is given, otherwise a new array.
|
|
204
|
+
# @raise [CArray::JIT::Unsupported] when the block falls outside the
|
|
205
|
+
# recognized subset, when no array is given, or when both `type` and
|
|
206
|
+
# `into` are.
|
|
207
|
+
def self.jit_stencil (*arrays, border: :mask, type: nil, into: nil, &block)
|
|
208
|
+
unless block
|
|
209
|
+
raise JIT::Unsupported, "jit_stencil needs a block"
|
|
210
|
+
end
|
|
211
|
+
if arrays.empty?
|
|
212
|
+
raise JIT::Unsupported,
|
|
213
|
+
"jit_stencil takes the arrays its block has windows onto, as in " \
|
|
214
|
+
"`CArray.jit_stencil(image) { |a| ... }`"
|
|
215
|
+
end
|
|
216
|
+
JIT.run_stencil(arrays, block, border, type, into)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Returns the contraction the block writes: an index that appears twice
|
|
220
|
+
# is summed.
|
|
221
|
+
#
|
|
222
|
+
# CArray.jit_contract { |i, j, k| c[i,j] = a[i,k] * b[k,j] }
|
|
223
|
+
#
|
|
224
|
+
# Every block parameter is an index. The ones that appear on the left are
|
|
225
|
+
# the cells written; the rest -- `k` here -- are summed over.
|
|
226
|
+
#
|
|
227
|
+
# No extent is given, because every index's extent is fixed by the axes it
|
|
228
|
+
# addresses; an index whose axes disagree is an error, which is the shape
|
|
229
|
+
# check a contraction needs.
|
|
230
|
+
#
|
|
231
|
+
# With no assignment the result is allocated and returned, with the free
|
|
232
|
+
# indices as its axes in the order the block named them:
|
|
233
|
+
#
|
|
234
|
+
# c = CArray.jit_contract { |i, j, k| a[i,k] * b[k,j] }
|
|
235
|
+
#
|
|
236
|
+
# Assigning into an array of your own says where to put it, and in what
|
|
237
|
+
# order its axes lie; it does not decide what is summed.
|
|
238
|
+
#
|
|
239
|
+
# The block is read and compiled, never called, so it is not yielded to.
|
|
240
|
+
#
|
|
241
|
+
# @return [CArray, CArray::JIT::CompiledKernel] the allocated result when the block
|
|
242
|
+
# assigns into nothing, otherwise the compiled kernel.
|
|
243
|
+
# @raise [CArray::JIT::Unsupported] when the block falls outside the
|
|
244
|
+
# recognized subset, or an index's axes disagree.
|
|
245
|
+
def self.jit_contract (&block)
|
|
246
|
+
unless block
|
|
247
|
+
raise JIT::Unsupported, "jit_contract needs a block"
|
|
248
|
+
end
|
|
249
|
+
JIT.run_contraction(block)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# @!endgroup
|
|
253
|
+
|
|
254
|
+
# @!endgroup
|
|
255
|
+
|
|
256
|
+
# @!group C functions
|
|
257
|
+
|
|
258
|
+
# Returns a callable for a C function someone else compiled, named by
|
|
259
|
+
# quoting its declaration, so that a kernel body -- or Ruby -- can call it:
|
|
260
|
+
#
|
|
261
|
+
# j0 = CArray.jit_extern("double j0(double)", from: "libgsl")
|
|
262
|
+
# CArray.jit_each { out = j0.call(x) }
|
|
263
|
+
#
|
|
264
|
+
# Nothing is compiled here: `extern` is C's word for a body that lives
|
|
265
|
+
# elsewhere, and finding it is Fiddle's job. What this gem adds is that the
|
|
266
|
+
# kernel calls the address directly instead of reaching it per cell through
|
|
267
|
+
# Fiddle.
|
|
268
|
+
#
|
|
269
|
+
# @param prototype [String] the function's C declaration, as C writes it.
|
|
270
|
+
# @param from [String, nil] the library to open; `nil` searches the process.
|
|
271
|
+
# @return [CArray::JIT::CFunction]
|
|
272
|
+
def self.jit_extern (prototype, from: nil, &block)
|
|
273
|
+
JIT.extern(prototype, from: from, &block)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Returns a callable for a C function of your own, written in Ruby and
|
|
277
|
+
# compiled here:
|
|
278
|
+
#
|
|
279
|
+
# smoothstep = CArray.jit_function("double (*)(double)") { |t| t * t }
|
|
280
|
+
#
|
|
281
|
+
# It is called from a kernel like any other, which gives kernels a body that
|
|
282
|
+
# can be factored and named, and its address may be handed to a C library
|
|
283
|
+
# that knows nothing about Ruby.
|
|
284
|
+
#
|
|
285
|
+
# The block is read and compiled, never called, so it is not yielded to.
|
|
286
|
+
#
|
|
287
|
+
# @param prototype [String] the function's C declaration, as C writes it.
|
|
288
|
+
# @return [CArray::JIT::CFunction]
|
|
289
|
+
# @raise [CArray::JIT::Unsupported] when the block falls outside the
|
|
290
|
+
# recognized subset.
|
|
291
|
+
def self.jit_function (prototype, &block)
|
|
292
|
+
JIT.function(prototype, &block)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# @!endgroup
|
|
296
|
+
|
|
297
|
+
# The compiler behind `CArray.jit_*`: it reads a block, generates C for it,
|
|
298
|
+
# builds it and calls it. What is public here is the reassociation default
|
|
299
|
+
# and the kernel cache; the rest is the machinery.
|
|
300
|
+
module JIT
|
|
301
|
+
|
|
302
|
+
class << self
|
|
303
|
+
|
|
304
|
+
# Whether a reduction's accumulator may be split into partial sums when
|
|
305
|
+
# the call site does not say. True: a kernel is here to be fast, and
|
|
306
|
+
# splitting the accumulation is usually the more accurate answer as well
|
|
307
|
+
# as the faster one.
|
|
308
|
+
#
|
|
309
|
+
# What it does not give is the order a serial Ruby loop takes, so it is
|
|
310
|
+
# false that says "compute this exactly as the loop would" -- for a
|
|
311
|
+
# compensated summation, or to check one against the other.
|
|
312
|
+
# CARRAY_JIT_REASSOCIATE=0 sets it false for a whole process, which is
|
|
313
|
+
# how this gem's own tests compare against Ruby.
|
|
314
|
+
# @!attribute [w] reassociate
|
|
315
|
+
# Sets whether a reduction's accumulator may be split into partial
|
|
316
|
+
# sums when the call site does not say.
|
|
317
|
+
# @return [Boolean]
|
|
318
|
+
attr_writer :reassociate
|
|
319
|
+
|
|
320
|
+
# @return [Boolean] whether a reduction's accumulator may be split when
|
|
321
|
+
# the call site does not say.
|
|
322
|
+
def reassociate
|
|
323
|
+
return @reassociate unless @reassociate.nil?
|
|
324
|
+
@reassociate = ENV["CARRAY_JIT_REASSOCIATE"] != "0"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# @private
|
|
328
|
+
RESULT = :__contraction_result
|
|
329
|
+
|
|
330
|
+
# @private
|
|
331
|
+
def run_contraction (block)
|
|
332
|
+
node, source, origin = read_block(block)
|
|
333
|
+
names = capture_names(source, node)
|
|
334
|
+
arrays, scalars, c_functions = split_captures(names, binding_of(block))
|
|
335
|
+
|
|
336
|
+
result = allocate_result(source, node, arrays, scalars)
|
|
337
|
+
arrays = arrays.merge(RESULT => result) if result
|
|
338
|
+
|
|
339
|
+
kernel = compile(source,
|
|
340
|
+
node: node,
|
|
341
|
+
origin: origin,
|
|
342
|
+
array_names: arrays.keys,
|
|
343
|
+
storage_types: arrays.transform_values(&:data_type_name),
|
|
344
|
+
scalar_values: scalars,
|
|
345
|
+
c_functions: c_functions,
|
|
346
|
+
masked: arrays.each_value.any? { |array| array.has_mask? },
|
|
347
|
+
contract: true,
|
|
348
|
+
result: RESULT,
|
|
349
|
+
cell_names: cell_names(arrays))
|
|
350
|
+
|
|
351
|
+
extents = contraction_extents(kernel, arrays)
|
|
352
|
+
if kernel.masked
|
|
353
|
+
kernel.written_arrays.each do |name|
|
|
354
|
+
array = arrays.fetch(name)
|
|
355
|
+
array.mask = 0 unless array.has_mask?
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
kernel.call(arrays, scalars, extents, c_functions)
|
|
359
|
+
result || kernel
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# A contraction with nothing to assign into needs its result sized and
|
|
363
|
+
# typed before there is a kernel to ask, so the block is analyzed once
|
|
364
|
+
# without being compiled. Returns nil when the block assigns into an
|
|
365
|
+
# array of its own.
|
|
366
|
+
def allocate_result (source, node, arrays, scalars)
|
|
367
|
+
probe = probe_contraction(source, node, arrays, scalars)
|
|
368
|
+
return nil unless probe
|
|
369
|
+
free, index_axes, type = probe
|
|
370
|
+
shape = free.map do |index|
|
|
371
|
+
extents = index_axes.fetch(index).map { |array, axis|
|
|
372
|
+
[arrays.fetch(array).dim[axis], array, axis]
|
|
373
|
+
}
|
|
374
|
+
distinct = extents.map(&:first).uniq
|
|
375
|
+
if distinct.size > 1
|
|
376
|
+
described = extents.map { |extent, array, axis|
|
|
377
|
+
"`#{array}` axis #{axis} is #{extent}"
|
|
378
|
+
}.join(", ")
|
|
379
|
+
raise Unsupported,
|
|
380
|
+
"`#{index}` addresses axes of different extents: #{described}"
|
|
381
|
+
end
|
|
382
|
+
distinct.first
|
|
383
|
+
end
|
|
384
|
+
CArray.send(type, *(shape.empty? ? [1] : shape))
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# @private
|
|
388
|
+
def probe_contraction (source, node, arrays, scalars)
|
|
389
|
+
key = [source, arrays.transform_values(&:data_type_name),
|
|
390
|
+
scalars.transform_values { |value| TypeAssignment.scalar_type(value) },
|
|
391
|
+
cell_names(arrays)]
|
|
392
|
+
cached = probe_cache[key]
|
|
393
|
+
return cached unless cached.nil?
|
|
394
|
+
probe_cache[key] = build_probe(source, node, arrays, scalars)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# @private
|
|
398
|
+
def probe_cache
|
|
399
|
+
@probe_cache ||= {}
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# @private
|
|
403
|
+
def build_probe (source, node, arrays, scalars)
|
|
404
|
+
storage_types = arrays.transform_values(&:data_type_name)
|
|
405
|
+
analyzer = Analyzer.new(source, node: node, array_names: arrays.keys,
|
|
406
|
+
contract: :probe,
|
|
407
|
+
cell_names: cell_names(arrays))
|
|
408
|
+
return false if analyzer.body.statements.last.is_a?(ElementWrite)
|
|
409
|
+
|
|
410
|
+
TypeAssignment.new(analyzer.body, storage_types, scalars)
|
|
411
|
+
summand = analyzer.body.statements.last
|
|
412
|
+
axes = Hash.new { |hash, key| hash[key] = [] }
|
|
413
|
+
analyzer.subscripts.each do |array, uses|
|
|
414
|
+
uses.each do |per_axis|
|
|
415
|
+
per_axis.each_with_index do |(index, _), axis|
|
|
416
|
+
axes[index] << [array, axis] if index
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
[analyzer.probe_free_names, axes,
|
|
421
|
+
{ :double => :float64, :complex => :cmplx128 }
|
|
422
|
+
.fetch(summand.type, :int64)]
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Each index's extent comes from the axes it addresses. Where it
|
|
426
|
+
# addresses more than one and they differ, the contraction has no shape
|
|
427
|
+
# and says which axes disagreed.
|
|
428
|
+
def contraction_extents (kernel, arrays)
|
|
429
|
+
order = kernel.index_names + kernel.contracted_names
|
|
430
|
+
order.map do |index|
|
|
431
|
+
places = kernel.index_axes[index]
|
|
432
|
+
extents = places.map { |array, axis| [arrays.fetch(array).dim[axis], array, axis] }
|
|
433
|
+
distinct = extents.map(&:first).uniq
|
|
434
|
+
if distinct.size > 1
|
|
435
|
+
described = extents.map { |extent, array, axis|
|
|
436
|
+
"`#{array}` axis #{axis} is #{extent}"
|
|
437
|
+
}.join(", ")
|
|
438
|
+
raise Unsupported,
|
|
439
|
+
"`#{index}` addresses axes of different extents: #{described}"
|
|
440
|
+
end
|
|
441
|
+
[0, distinct.first, 1]
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# @private
|
|
446
|
+
MAP_RESULT = :__map_result
|
|
447
|
+
|
|
448
|
+
# Whether CArray's sweep can run this pass, decided before there is a
|
|
449
|
+
# kernel -- because the answer changes what is compiled.
|
|
450
|
+
#
|
|
451
|
+
# A chunk is one flat run of cells, and CArray already treats an operand
|
|
452
|
+
# as one: its acquire reads `elements` and the element size and never
|
|
453
|
+
# looks at the shape. So the arrays do not have to be flattened, and
|
|
454
|
+
# are not; what has to be flat is the *kernel*, which is compiled at
|
|
455
|
+
# rank one over the same cells rather than as a nest over the axes.
|
|
456
|
+
#
|
|
457
|
+
# The one thing that cannot be flattened is a stretched operand.
|
|
458
|
+
# Broadcasting arrives as a stride of zero on an axis, and an axis is
|
|
459
|
+
# what a flat run has none of -- so cell k of the output would stop
|
|
460
|
+
# lining up with cell k of the stretched operand. Hence the test is
|
|
461
|
+
# that every operand already has the shape the pass covers, which is
|
|
462
|
+
# stricter than agreeing on the count of cells.
|
|
463
|
+
def sweepable_pass? (arrays, shape, masked)
|
|
464
|
+
return false unless Sweep.available?
|
|
465
|
+
return false if masked
|
|
466
|
+
return false if arrays.empty? || arrays.size > Sweep::MAX_ARITY
|
|
467
|
+
return false unless arrays.each_value.all? { |array|
|
|
468
|
+
array.rank.zero? || array.dim == shape
|
|
469
|
+
}
|
|
470
|
+
walkable_in_place?(arrays.each_value)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Whether the operands settle the question the shape leaves open.
|
|
474
|
+
#
|
|
475
|
+
# The sweep re-gathers what it cannot walk in place, 32KB at a time,
|
|
476
|
+
# and that is a bargain against materialising the same operand whole --
|
|
477
|
+
# but only where the tiers here would have to materialise it. So the
|
|
478
|
+
# tier each operand would be opened at is the rest of the decision:
|
|
479
|
+
#
|
|
480
|
+
# TIER_ATTACH neither can walk it, and the sweep holds 32KB where
|
|
481
|
+
# the tiers hold the whole box: the sweep runs
|
|
482
|
+
# TIER_STRIDE a column, a transpose, every other cell -- the tiers
|
|
483
|
+
# address it in place, and the sweep re-gathers it for
|
|
484
|
+
# nothing: the driver stays here
|
|
485
|
+
# TIER_ENTITY both walk the buffer, and the two are
|
|
486
|
+
# indistinguishable: the sweep runs, as it always did
|
|
487
|
+
#
|
|
488
|
+
# Measured over two million doubles with a strided view as the operand:
|
|
489
|
+
# 0.9 ns/cell with the driver here against 6.1 ns/cell swept, and no
|
|
490
|
+
# scratch either way -- there was nothing the re-gather was buying.
|
|
491
|
+
def walkable_in_place? (arrays)
|
|
492
|
+
tiers = arrays.map { |array| Access.classify(array)[:tier] }
|
|
493
|
+
return true if tiers.include?(Access::TIER_ATTACH)
|
|
494
|
+
tiers.none? { |tier| tier == Access::TIER_STRIDE }
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Which loop runs it. Both compute the same thing from the same
|
|
498
|
+
# compiled body -- what differs is who acquires the operands.
|
|
499
|
+
#
|
|
500
|
+
# CArray's chunked sweep re-gathers an operand it cannot walk in place
|
|
501
|
+
# 32KB at a time, where the tiers here move the whole box the kernel
|
|
502
|
+
# touches, and for an element-wise pass that box is the whole array.
|
|
503
|
+
# Measured over two million doubles with a gather view as an operand:
|
|
504
|
+
# 1.3 ms either way, and 32KB of scratch against sixteen megabytes.
|
|
505
|
+
# With an entity operand the two are indistinguishable, so there is
|
|
506
|
+
# nothing to weigh. With a strided view there is: the tiers address it
|
|
507
|
+
# in place and the sweep re-gathers it, which is why #sweepable_pass?
|
|
508
|
+
# asks what tier each operand would be opened at and not only what
|
|
509
|
+
# shape it has.
|
|
510
|
+
#
|
|
511
|
+
# It cannot always. A body that asks about a mask has no per-cell mask
|
|
512
|
+
# in a chunk to ask it of; operands that disagree on their element count
|
|
513
|
+
# are broadcast here and refused there; and a kernel of rank two or more
|
|
514
|
+
# addresses its operands by axis, which one flat run of cells cannot
|
|
515
|
+
# supply.
|
|
516
|
+
def drive (kernel, aligned, scalars, c_functions, shape, sweeping)
|
|
517
|
+
if sweeping
|
|
518
|
+
kernel.sweep(aligned, scalars, c_functions)
|
|
519
|
+
else
|
|
520
|
+
kernel.call(aligned, scalars, shape.map { |extent| [0, extent, 1] },
|
|
521
|
+
c_functions)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# What the frame gets. Two kinds: `:mask` and `:skip` are answers about
|
|
526
|
+
# the frame cells themselves -- mark them, or leave them -- and nothing
|
|
527
|
+
# is computed for a cell that was not computed. The other three say
|
|
528
|
+
# what a read outside the array gives, so the frame is computed after
|
|
529
|
+
# all, by a second walk with that rule written into its reads.
|
|
530
|
+
FRAME_BORDERS = [:mask, :skip].freeze
|
|
531
|
+
# @private
|
|
532
|
+
COMPUTED_BORDERS = [:zero, :clamp, :wrap].freeze
|
|
533
|
+
|
|
534
|
+
# @private
|
|
535
|
+
def run_stencil (arrays, block, border, type, into)
|
|
536
|
+
unless FRAME_BORDERS.include?(border) || COMPUTED_BORDERS.include?(border)
|
|
537
|
+
raise Unsupported,
|
|
538
|
+
"`border:` is " \
|
|
539
|
+
"#{(FRAME_BORDERS + COMPUTED_BORDERS).map(&:inspect).join(', ')}, " \
|
|
540
|
+
"got #{border.inspect}"
|
|
541
|
+
end
|
|
542
|
+
if type && into
|
|
543
|
+
raise Unsupported,
|
|
544
|
+
"`into:` names an array, which already says what type it is; " \
|
|
545
|
+
"pass one or the other"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
node, source, origin = read_block(block)
|
|
549
|
+
windows = block.parameters.map(&:last)
|
|
550
|
+
unless windows.size == arrays.size
|
|
551
|
+
raise Unsupported,
|
|
552
|
+
"the block takes #{windows.size} " \
|
|
553
|
+
"#{windows.size == 1 ? 'window' : 'windows'}, and " \
|
|
554
|
+
"#{arrays.size} #{arrays.size == 1 ? 'array was' : 'arrays were'} " \
|
|
555
|
+
"given"
|
|
556
|
+
end
|
|
557
|
+
arrays.each_with_index do |array, position|
|
|
558
|
+
unless array.is_a?(CArray)
|
|
559
|
+
raise Unsupported,
|
|
560
|
+
"jit_stencil takes arrays; `#{windows[position]}` was given " \
|
|
561
|
+
"#{array.class}"
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
shape = arrays.first.dim
|
|
565
|
+
arrays.each_with_index do |array, position|
|
|
566
|
+
next if array.dim == shape
|
|
567
|
+
# Shapes are required to agree rather than broadcast: a stretched
|
|
568
|
+
# axis has no neighbour to reach, so what a window would mean on one
|
|
569
|
+
# is a question this does not have to answer yet.
|
|
570
|
+
raise Unsupported,
|
|
571
|
+
"`#{windows.first}` has shape #{shape.inspect} and " \
|
|
572
|
+
"`#{windows[position]}` #{array.dim.inspect}; a stencil's " \
|
|
573
|
+
"arrays have the same shape"
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
windowed = windows.zip(arrays).to_h
|
|
577
|
+
free, assigned = free_and_assigned_names(source, node)
|
|
578
|
+
captured, scalars, c_functions =
|
|
579
|
+
split_captures(free - windows, binding_of(block))
|
|
580
|
+
unless assigned.empty? || (assigned & captured.keys).empty?
|
|
581
|
+
raise Unsupported,
|
|
582
|
+
"a stencil's value is its block's, and the cells it is over " \
|
|
583
|
+
"are windows; an array it writes belongs to `jit_each`"
|
|
584
|
+
end
|
|
585
|
+
given = windowed.merge(captured)
|
|
586
|
+
|
|
587
|
+
result = stencil_result(source, node, given, scalars, c_functions,
|
|
588
|
+
windows, shape, type, into)
|
|
589
|
+
# `:mask` does not make this a masked kernel. The frame is marked
|
|
590
|
+
# before the loop runs and the loop never reaches it, so what the
|
|
591
|
+
# kernel is asked to carry is what the operands carry -- and a masked
|
|
592
|
+
# kernel reads and ORs a mask byte per cell, which the interior of an
|
|
593
|
+
# unmasked stencil has no reason to pay for.
|
|
594
|
+
masked = given.each_value.any? { |array| array.has_mask? } ||
|
|
595
|
+
mentions_undef(source, node)
|
|
596
|
+
kernel = compile(source,
|
|
597
|
+
node: node,
|
|
598
|
+
origin: origin,
|
|
599
|
+
array_names: given.keys + [MAP_RESULT],
|
|
600
|
+
storage_types: given.merge(MAP_RESULT => result)
|
|
601
|
+
.transform_values(&:data_type_name),
|
|
602
|
+
scalar_values: scalars,
|
|
603
|
+
c_functions: c_functions,
|
|
604
|
+
masked: masked,
|
|
605
|
+
rank: shape.size,
|
|
606
|
+
windows: windows,
|
|
607
|
+
border: COMPUTED_BORDERS.include?(border) ? border : nil,
|
|
608
|
+
map: true,
|
|
609
|
+
result: MAP_RESULT)
|
|
610
|
+
|
|
611
|
+
# The interior is where every window is inside the array; the frame is
|
|
612
|
+
# what is left, and what `border:` answers for.
|
|
613
|
+
reach = kernel.window_reach
|
|
614
|
+
bounds = shape.each_with_index.map { |extent, axis|
|
|
615
|
+
low, high = reach[axis]
|
|
616
|
+
[-low, extent - high, 1]
|
|
617
|
+
}
|
|
618
|
+
if bounds.any? { |from, to, _| from >= to }
|
|
619
|
+
raise Unsupported,
|
|
620
|
+
"the window reaches #{reach.inspect} and the arrays are " \
|
|
621
|
+
"#{shape.inspect}, so there is no cell where the window is " \
|
|
622
|
+
"inside the array"
|
|
623
|
+
end
|
|
624
|
+
result.mask = 0 if (kernel.masked || border == :mask) && !result.has_mask?
|
|
625
|
+
mark_frame(result, bounds, shape) if border == :mask
|
|
626
|
+
operands = given.merge(MAP_RESULT => result)
|
|
627
|
+
kernel.call(operands, scalars, bounds, c_functions)
|
|
628
|
+
if COMPUTED_BORDERS.include?(border)
|
|
629
|
+
frame_boxes(bounds, shape).each do |box|
|
|
630
|
+
kernel.call(operands, scalars, box, c_functions, border: true)
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
result
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# The frame, cut into boxes the loop can walk. Peeling one axis at a
|
|
637
|
+
# time and taking the interior of the axes already peeled is what keeps
|
|
638
|
+
# them from overlapping: a cell in two of them would be computed twice,
|
|
639
|
+
# and a stencil that wrote a cell twice would be a different thing
|
|
640
|
+
# depending on which write landed last.
|
|
641
|
+
#
|
|
642
|
+
# Two boxes per axis, so 2 * rank of them however wide the window is --
|
|
643
|
+
# the corners come with the axis peeled first rather than being cases of
|
|
644
|
+
# their own.
|
|
645
|
+
def frame_boxes (bounds, shape)
|
|
646
|
+
boxes = []
|
|
647
|
+
shape.each_index do |axis|
|
|
648
|
+
from, to, = bounds[axis]
|
|
649
|
+
[[0, from], [to, shape[axis]]].each do |low, high|
|
|
650
|
+
next if low >= high
|
|
651
|
+
box = shape.each_index.map { |other|
|
|
652
|
+
if other == axis then [low, high, 1]
|
|
653
|
+
elsif other < axis then bounds[other]
|
|
654
|
+
else [0, shape[other], 1]
|
|
655
|
+
end
|
|
656
|
+
}
|
|
657
|
+
boxes << box
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
boxes
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# A frame cell is one the loop does not write, so `:mask` marks it
|
|
664
|
+
# before the loop rather than during it: what says "not computed" is a
|
|
665
|
+
# mask byte, and the cells are named by the same bounds the loop is
|
|
666
|
+
# given. This is the whole of `:mask` -- there is nothing to compute
|
|
667
|
+
# for a cell that was not computed.
|
|
668
|
+
def mark_frame (result, bounds, shape)
|
|
669
|
+
shape.each_with_index do |extent, axis|
|
|
670
|
+
from, to, = bounds[axis]
|
|
671
|
+
[(0...from), (to...extent)].each do |span|
|
|
672
|
+
next if span.size.zero?
|
|
673
|
+
index = Array.new(shape.size) { nil }
|
|
674
|
+
index[axis] = span
|
|
675
|
+
result[*index] = UNDEF
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# Typed from the block's value, as jit_map's result is, unless the
|
|
681
|
+
# caller said otherwise. `into:` is checked against the shape here
|
|
682
|
+
# rather than by the kernel, so that a wrong array is refused before
|
|
683
|
+
# anything is compiled for it.
|
|
684
|
+
def stencil_result (source, node, arrays, scalars, c_functions, windows,
|
|
685
|
+
shape, type, into)
|
|
686
|
+
if into
|
|
687
|
+
unless into.is_a?(CArray) && into.dim == shape
|
|
688
|
+
raise Unsupported,
|
|
689
|
+
"`into:` takes an array of the stencil's own shape " \
|
|
690
|
+
"#{shape.inspect}"
|
|
691
|
+
end
|
|
692
|
+
return into
|
|
693
|
+
end
|
|
694
|
+
chosen = type || probe_stencil(source, node, arrays, scalars,
|
|
695
|
+
c_functions, windows, shape)
|
|
696
|
+
CArray.send(chosen, *shape)
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# @private
|
|
700
|
+
def probe_stencil (source, node, arrays, scalars, c_functions, windows,
|
|
701
|
+
shape)
|
|
702
|
+
key = [:stencil, source, arrays.transform_values(&:data_type_name),
|
|
703
|
+
scalars.transform_values { |value| TypeAssignment.scalar_type(value) },
|
|
704
|
+
c_functions.transform_values(&:signature), shape.size]
|
|
705
|
+
cached = probe_cache[key]
|
|
706
|
+
return cached if cached
|
|
707
|
+
analyzer = Analyzer.new(source, node: node, array_names: arrays.keys,
|
|
708
|
+
c_functions: c_functions, rank: shape.size,
|
|
709
|
+
windows: windows, map: :probe)
|
|
710
|
+
TypeAssignment.new(analyzer.body, arrays.transform_values(&:data_type_name),
|
|
711
|
+
scalars, c_functions)
|
|
712
|
+
probe_cache[key] =
|
|
713
|
+
TypeAssignment.result_storage_type(analyzer.body.statements.last.type)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# The result has to be sized and typed before there is a kernel to ask,
|
|
717
|
+
# so the block is analyzed once without being compiled -- the same thing
|
|
718
|
+
# a returned contraction does, for the same reason.
|
|
719
|
+
def allocate_map_result (source, node, arrays, scalars, shape, c_functions)
|
|
720
|
+
type = probe_map(source, node, arrays, scalars, c_functions)
|
|
721
|
+
CArray.send(type, *(shape.empty? ? [1] : shape))
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# @private
|
|
725
|
+
def probe_map (source, node, arrays, scalars, c_functions)
|
|
726
|
+
key = [:map, source, arrays.transform_values(&:data_type_name),
|
|
727
|
+
scalars.transform_values { |value| TypeAssignment.scalar_type(value) },
|
|
728
|
+
c_functions.transform_values(&:signature)]
|
|
729
|
+
cached = probe_cache[key]
|
|
730
|
+
return cached if cached
|
|
731
|
+
analyzer = Analyzer.new(source, node: node, array_names: arrays.keys,
|
|
732
|
+
c_functions: c_functions, rank: 1, map: :probe)
|
|
733
|
+
TypeAssignment.new(analyzer.body, arrays.transform_values(&:data_type_name),
|
|
734
|
+
scalars, c_functions)
|
|
735
|
+
value = analyzer.body.statements.last
|
|
736
|
+
probe_cache[key] = TypeAssignment.result_storage_type(value.type)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# @private
|
|
740
|
+
def run_over_whole_arrays (block, map: false)
|
|
741
|
+
node, source, origin = read_block(block)
|
|
742
|
+
free, assigned = free_and_assigned_names(source, node)
|
|
743
|
+
arrays, scalars, c_functions = split_captures(free, binding_of(block))
|
|
744
|
+
# `out = a + b` writes the array named `out` where the block was
|
|
745
|
+
# written. A name the block assigns is not free in it, so it is
|
|
746
|
+
# looked up here rather than by split_captures -- and a name that is
|
|
747
|
+
# not an array outside stays what it looks like, a local.
|
|
748
|
+
arrays = arrays.merge(assigned_arrays(assigned, binding_of(block)))
|
|
749
|
+
if arrays.empty?
|
|
750
|
+
raise Unsupported, "the block reaches no array"
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
aligned, shape = broadcast(arrays)
|
|
754
|
+
if map
|
|
755
|
+
result = allocate_map_result(source, node, arrays, scalars, shape,
|
|
756
|
+
c_functions)
|
|
757
|
+
arrays = arrays.merge(MAP_RESULT => result)
|
|
758
|
+
aligned = aligned.merge(MAP_RESULT => result)
|
|
759
|
+
end
|
|
760
|
+
masked = arrays.each_value.any? { |array| array.has_mask? } ||
|
|
761
|
+
mentions_undef(source, node)
|
|
762
|
+
sweeping = sweepable_pass?(arrays, shape, masked)
|
|
763
|
+
kernel = compile(source,
|
|
764
|
+
node: node,
|
|
765
|
+
origin: origin,
|
|
766
|
+
array_names: arrays.keys,
|
|
767
|
+
storage_types: arrays.transform_values(&:data_type_name),
|
|
768
|
+
scalar_values: scalars,
|
|
769
|
+
c_functions: c_functions,
|
|
770
|
+
masked: masked,
|
|
771
|
+
rank: sweeping ? 1 : shape.size,
|
|
772
|
+
map: map,
|
|
773
|
+
result: map ? MAP_RESULT : nil)
|
|
774
|
+
|
|
775
|
+
kernel.written_arrays.each do |name|
|
|
776
|
+
next if name == MAP_RESULT
|
|
777
|
+
written = arrays.fetch(name)
|
|
778
|
+
unless written.dim == shape
|
|
779
|
+
raise Unsupported,
|
|
780
|
+
"`#{name}` has shape #{written.dim.inspect}, but the " \
|
|
781
|
+
"expression covers #{shape.inspect}; a stretched array " \
|
|
782
|
+
"cannot be written to"
|
|
783
|
+
end
|
|
784
|
+
aligned[name] = written
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
if kernel.masked
|
|
788
|
+
kernel.written_arrays.each do |name|
|
|
789
|
+
array = arrays.fetch(name)
|
|
790
|
+
array.mask = 0 unless array.has_mask?
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
result.mask = 0 if map && kernel.masked && !result.has_mask?
|
|
795
|
+
drive(kernel, aligned, scalars, c_functions, shape, sweeping)
|
|
796
|
+
map ? result : kernel
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# CArray lines the shapes up; a stretched axis comes back as a stride of
|
|
800
|
+
# zero, which the kernel addresses like any other stride.
|
|
801
|
+
# CArray leaves a CScalar as it is, because its own kernels know to read
|
|
802
|
+
# one cell of it for every cell of everything else. This loop does not
|
|
803
|
+
# know that -- it addresses what it is handed -- so the CScalar is first
|
|
804
|
+
# referred to as the one-cell array it already is, and comes back from
|
|
805
|
+
# the broadcast as the stretched view a one-cell CArray comes back as.
|
|
806
|
+
# The referred view is the same memory, so a write still lands.
|
|
807
|
+
#
|
|
808
|
+
# One axis per axis of what it is standing beside, rather than the one
|
|
809
|
+
# axis it would have had on its own: `CArray.broadcast` stretches a
|
|
810
|
+
# size-1 axis but does not invent an axis that is missing, so `[1]`
|
|
811
|
+
# against a `[2, 3]` is an ndim mismatch rather than a scalar. A
|
|
812
|
+
# CScalar has no shape of its own to contradict this -- being shapeless
|
|
813
|
+
# is what it is for -- so it takes the rank of the operands that do,
|
|
814
|
+
# and stretches on every axis, which is what CArray's own operators
|
|
815
|
+
# give for the same expression.
|
|
816
|
+
def broadcast (arrays)
|
|
817
|
+
values = arrays.values
|
|
818
|
+
rank = values.reject { |value| value.is_a?(CScalar) }
|
|
819
|
+
.map(&:rank).max || 1
|
|
820
|
+
values = values.map { |value|
|
|
821
|
+
value.is_a?(CScalar) ? value.refer(value.data_type, [1] * rank) : value
|
|
822
|
+
}
|
|
823
|
+
aligned = values.size == 1 ? values : CArray.broadcast(*values)
|
|
824
|
+
[arrays.keys.zip(aligned).to_h, aligned.first.dim]
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# An indexed kernel addresses what it is handed, so a CScalar among the
|
|
828
|
+
# captures is named here rather than stretched: it has no axis to walk,
|
|
829
|
+
# and the block says so by writing no index for it. The whole-array
|
|
830
|
+
# spellings have no such name to write, and broadcast instead.
|
|
831
|
+
def cell_names (arrays)
|
|
832
|
+
arrays.select { |_, value| value.is_a?(CScalar) }.keys
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# @private
|
|
836
|
+
def binding_of (block)
|
|
837
|
+
block.binding
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# @private
|
|
841
|
+
def run (extents, block, reassociate = nil)
|
|
842
|
+
node, source, origin = read_block(block)
|
|
843
|
+
names = capture_names(source, node)
|
|
844
|
+
arrays, scalars, c_functions = split_captures(names, binding_of(block))
|
|
845
|
+
|
|
846
|
+
# A plain CArray carries no mask; one exists only once a cell has
|
|
847
|
+
# actually been marked. So masks are touched at all only when some
|
|
848
|
+
# array already has one -- and then the arrays being written need one
|
|
849
|
+
# too, which is what CArray's own operators do.
|
|
850
|
+
if extents.empty?
|
|
851
|
+
count = block.arity
|
|
852
|
+
raise Unsupported,
|
|
853
|
+
"this block names #{count == 1 ? 'an index' : "#{count} indices"}, " \
|
|
854
|
+
"so it needs #{count == 1 ? 'an extent' : 'one extent each'}; " \
|
|
855
|
+
"pass a Range, a count or `(high - 1).step(low, -1)`, or drop " \
|
|
856
|
+
"the #{count == 1 ? 'index' : 'indices'} and write the arrays " \
|
|
857
|
+
"whole as `a[]`"
|
|
858
|
+
end
|
|
859
|
+
pairs = bounds(extents, extents.size)
|
|
860
|
+
steps = pairs.map { |triple, _| triple[2] }
|
|
861
|
+
|
|
862
|
+
kernel = compile(source,
|
|
863
|
+
node: node,
|
|
864
|
+
origin: origin,
|
|
865
|
+
array_names: arrays.keys,
|
|
866
|
+
storage_types: arrays.transform_values(&:data_type_name),
|
|
867
|
+
scalar_values: scalars,
|
|
868
|
+
c_functions: c_functions,
|
|
869
|
+
masked: arrays.each_value.any? { |array| array.has_mask? },
|
|
870
|
+
steps: steps,
|
|
871
|
+
cell_names: cell_names(arrays),
|
|
872
|
+
reassociate: reassociate.nil? ? JIT.reassociate : reassociate)
|
|
873
|
+
# The kernel decides, not the caller: mentioning UNDEF makes it a
|
|
874
|
+
# masked kernel even when no array carries a mask yet.
|
|
875
|
+
if kernel.masked
|
|
876
|
+
kernel.written_arrays.each do |name|
|
|
877
|
+
array = arrays.fetch(name)
|
|
878
|
+
array.mask = 0 unless array.has_mask?
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
unless pairs.size == kernel.rank
|
|
882
|
+
raise Unsupported,
|
|
883
|
+
"the block names #{kernel.rank} " \
|
|
884
|
+
"#{kernel.rank == 1 ? 'index' : 'indices'}, and " \
|
|
885
|
+
"#{pairs.size} #{pairs.size == 1 ? 'extent was' : 'extents were'} " \
|
|
886
|
+
"given"
|
|
887
|
+
end
|
|
888
|
+
kernel.call(arrays, scalars, pairs.map(&:first), c_functions)
|
|
889
|
+
kernel
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
# Compiles for one set of array data types and one set of scalar types,
|
|
893
|
+
# and returns the same CompiledKernel for every later call with the
|
|
894
|
+
# same ones. Nothing on this path is cheap relative to running the
|
|
895
|
+
# kernel, so all of it is memoized.
|
|
896
|
+
def compile (source, node: nil, origin: nil, array_names:, storage_types:,
|
|
897
|
+
scalar_values:, c_functions: {}, masked: false, rank: nil,
|
|
898
|
+
steps: nil, contract: false, result: nil, map: false,
|
|
899
|
+
reassociate: false, cell_names: [], windows: [], border: nil)
|
|
900
|
+
# A kernel that mentions UNDEF is a masked one whatever its arrays
|
|
901
|
+
# carry, and deciding that here means no caller has to remember it.
|
|
902
|
+
masked ||= mentions_undef(source, node)
|
|
903
|
+
key = [source, storage_types,
|
|
904
|
+
scalar_values.transform_values { |value|
|
|
905
|
+
TypeAssignment.scalar_type(value)
|
|
906
|
+
},
|
|
907
|
+
# The signature, not the address: `j0` and `y0` are the same
|
|
908
|
+
# kernel, and it is compiled once. One written here adds its
|
|
909
|
+
# symbol, which stands for its body -- see `CFunction#kernel_key`.
|
|
910
|
+
c_functions.transform_values(&:kernel_key),
|
|
911
|
+
masked, rank, steps, contract, result, map,
|
|
912
|
+
# Which names are read at their one cell rather than walked:
|
|
913
|
+
# the same source over a CScalar is a different kernel from
|
|
914
|
+
# the same source over a one-cell CArray.
|
|
915
|
+
cell_names,
|
|
916
|
+
# Which names are windows: the same source read as a stencil is
|
|
917
|
+
# not the same kernel as the same source read as a block that
|
|
918
|
+
# named its indices.
|
|
919
|
+
windows,
|
|
920
|
+
# And what a window that falls off the array reads: the rule is
|
|
921
|
+
# emitted into the frame's body, so two rules are two kernels.
|
|
922
|
+
border,
|
|
923
|
+
# The licence is part of the kernel, not of the call: it
|
|
924
|
+
# decides what C is emitted, so the two spellings are two
|
|
925
|
+
# kernels and the cache keeps them apart.
|
|
926
|
+
reassociate]
|
|
927
|
+
found = registry[key]
|
|
928
|
+
return found if found
|
|
929
|
+
registry[key] = build(source, node, array_names, storage_types,
|
|
930
|
+
scalar_values, c_functions, masked, rank, steps,
|
|
931
|
+
contract, result, origin, map, reassociate,
|
|
932
|
+
cell_names, windows, border)
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# A kernel that mentions UNDEF is a masked kernel whatever its arrays
|
|
936
|
+
# happen to carry: it asks about masks, or makes them. Checked here
|
|
937
|
+
# because the answer is needed before the kernel is built.
|
|
938
|
+
def mentions_undef (source, node = nil)
|
|
939
|
+
cached = undef_cache[source]
|
|
940
|
+
return cached unless cached.nil?
|
|
941
|
+
undef_cache[source] = Analyzer.mentions_undef?(source, node: node)
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
# @private
|
|
945
|
+
def undef_cache
|
|
946
|
+
@undef_cache ||= {}
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
# @private
|
|
950
|
+
def capture_names (source, node = nil)
|
|
951
|
+
free, = free_and_assigned_names(source, node)
|
|
952
|
+
free
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
# Which names the block reaches for, and which it assigns. Both are
|
|
956
|
+
# properties of the source, so both are cached; what each name *is* is
|
|
957
|
+
# a property of the binding, and is settled outside the cache.
|
|
958
|
+
def free_and_assigned_names (source, node = nil)
|
|
959
|
+
cached = capture_name_cache[source]
|
|
960
|
+
return cached if cached
|
|
961
|
+
capture_name_cache[source] =
|
|
962
|
+
Analyzer.free_and_assigned_names(source, node: node)
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# @private
|
|
966
|
+
def registry
|
|
967
|
+
@registry ||= {}
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
# @private
|
|
971
|
+
def capture_name_cache
|
|
972
|
+
@capture_name_cache ||= {}
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
# Keyed by instruction sequence, which CRuby hands back as the same
|
|
976
|
+
# object for every Proc made from one block literal. That makes it a
|
|
977
|
+
# free identity for the block, and keeps the file from being read and
|
|
978
|
+
# parsed again on each call.
|
|
979
|
+
def block_cache
|
|
980
|
+
@block_cache ||= {}
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
# @!group Kernel cache
|
|
984
|
+
|
|
985
|
+
# Forgets the kernels compiled in this process, so that the next call
|
|
986
|
+
# reaches the cache on disk rather than the one held in memory.
|
|
987
|
+
#
|
|
988
|
+
# @return [void]
|
|
989
|
+
def clear_registry
|
|
990
|
+
@registry = {}
|
|
991
|
+
@capture_name_cache = {}
|
|
992
|
+
@block_cache = {}
|
|
993
|
+
@undef_cache = {}
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
# @return [String] the directory this environment's kernels are kept in,
|
|
997
|
+
# named for the versions and architecture they were built for.
|
|
998
|
+
def cache_directory
|
|
999
|
+
Compiler.cache_directory
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
# @return [String] the directory holding one entry per environment.
|
|
1003
|
+
def cache_root
|
|
1004
|
+
Compiler.cache_root
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
# @return [Array<String>] the environment directories no longer in use --
|
|
1008
|
+
# another version, or another architecture.
|
|
1009
|
+
def stale_cache_environments
|
|
1010
|
+
Compiler.stale_environments
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
# @return [Integer] the number of kernels this environment has cached.
|
|
1014
|
+
def cache_entry_count
|
|
1015
|
+
Compiler.entry_count
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
# @return [Integer] the bytes this environment's cache holds.
|
|
1019
|
+
def cache_byte_size
|
|
1020
|
+
Compiler.byte_size
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
# Removes cached kernels.
|
|
1024
|
+
#
|
|
1025
|
+
# @param everything [Boolean] `true` to clear every environment, not
|
|
1026
|
+
# only this one.
|
|
1027
|
+
# @return [Integer] the number of entries removed.
|
|
1028
|
+
def clear_cache (everything: false)
|
|
1029
|
+
Compiler.clear(:everything => everything)
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
# @!endgroup
|
|
1033
|
+
|
|
1034
|
+
private
|
|
1035
|
+
|
|
1036
|
+
def build (source, node, array_names, storage_types, scalar_values, c_functions,
|
|
1037
|
+
masked, rank = nil, steps = nil, contract = false, result = nil,
|
|
1038
|
+
origin = nil, map = false, reassociate = false,
|
|
1039
|
+
cell_names = [], windows = [], border = nil)
|
|
1040
|
+
analyzer = Analyzer.new(source, node: node, array_names: array_names,
|
|
1041
|
+
c_functions: c_functions,
|
|
1042
|
+
rank: rank, steps: steps, contract: contract,
|
|
1043
|
+
result: result, map: map,
|
|
1044
|
+
cell_names: cell_names, windows: windows)
|
|
1045
|
+
assignment = TypeAssignment.new(analyzer.body, storage_types,
|
|
1046
|
+
scalar_values, c_functions)
|
|
1047
|
+
generator = CGenerator.new(analyzer, storage_types, assignment.scalar_types,
|
|
1048
|
+
c_functions: c_functions,
|
|
1049
|
+
masked: masked, reassociate: reassociate,
|
|
1050
|
+
steps: steps, border: border,
|
|
1051
|
+
origin: origin, block_source: source)
|
|
1052
|
+
CompiledKernel.new(source: source, generator: generator,
|
|
1053
|
+
analyzer: analyzer, storage_types: storage_types)
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
# Recovering a block's source means parsing the whole file it lives in,
|
|
1057
|
+
# which is far too expensive to repeat per call.
|
|
1058
|
+
def read_block (block)
|
|
1059
|
+
sequence = RubyVM::InstructionSequence.of(block) if
|
|
1060
|
+
defined?(RubyVM::InstructionSequence)
|
|
1061
|
+
return BlockReader.read(block) unless sequence
|
|
1062
|
+
cached = block_cache[sequence]
|
|
1063
|
+
return cached if cached
|
|
1064
|
+
block_cache[sequence] = BlockReader.read(block)
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
# What a block closes over is one of three things, and which it is
|
|
1068
|
+
# decides what the name means inside the kernel: an array is addressed
|
|
1069
|
+
# at a cell, a scalar is a constant the kernel is compiled with, and a
|
|
1070
|
+
# C function is something it calls.
|
|
1071
|
+
# Of the names the block assigns, the ones that are arrays where it was
|
|
1072
|
+
# written. Anything else -- a name that holds a number, or no name at
|
|
1073
|
+
# all -- is a local of the block's own.
|
|
1074
|
+
def assigned_arrays (names, binding)
|
|
1075
|
+
names.each_with_object({}) do |name, found|
|
|
1076
|
+
next unless binding.local_variable_defined?(name)
|
|
1077
|
+
value = binding.local_variable_get(name)
|
|
1078
|
+
found[name] = value if value.is_a?(CArray)
|
|
1079
|
+
end
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
def split_captures (names, binding)
|
|
1083
|
+
arrays = {}
|
|
1084
|
+
scalars = {}
|
|
1085
|
+
c_functions = {}
|
|
1086
|
+
names.each do |name|
|
|
1087
|
+
value = capture_value(name, binding)
|
|
1088
|
+
case value
|
|
1089
|
+
when CArray then arrays[name] = value
|
|
1090
|
+
when CFunction then c_functions[name] = value
|
|
1091
|
+
else scalars[name] = value
|
|
1092
|
+
end
|
|
1093
|
+
end
|
|
1094
|
+
[arrays, scalars, c_functions]
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
# A constant is looked up where the block was written, so it means what
|
|
1098
|
+
# it means there -- the enclosing module's, not the top level's. It is
|
|
1099
|
+
# the only name a method body can reach: `def` closes over nothing, so
|
|
1100
|
+
# a compiled function or a table held in a constant is how a method
|
|
1101
|
+
# gets at one.
|
|
1102
|
+
def capture_value (name, binding)
|
|
1103
|
+
if name.to_s.start_with?(/[A-Z]/)
|
|
1104
|
+
begin
|
|
1105
|
+
binding.eval(name.to_s)
|
|
1106
|
+
rescue NameError
|
|
1107
|
+
raise Unsupported,
|
|
1108
|
+
"`#{name}` is not defined where the block was written"
|
|
1109
|
+
end
|
|
1110
|
+
else
|
|
1111
|
+
unless binding.local_variable_defined?(name)
|
|
1112
|
+
refuse_a_draw(name)
|
|
1113
|
+
raise Unsupported,
|
|
1114
|
+
"`#{name}` is not defined where the block was written"
|
|
1115
|
+
end
|
|
1116
|
+
binding.local_variable_get(name)
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
# `rand` is Kernel's, so "not defined" would be a lie, and the reason it
|
|
1121
|
+
# is not here is worth saying where it is reached for.
|
|
1122
|
+
DRAW_NAMES = [:rand, :srand].freeze
|
|
1123
|
+
|
|
1124
|
+
def refuse_a_draw (name)
|
|
1125
|
+
return unless DRAW_NAMES.include?(name.to_sym)
|
|
1126
|
+
raise Unsupported, draw_message("`#{name}`")
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
# A generator has one state and hands out its numbers in the order it
|
|
1130
|
+
# was asked in, and a kernel does not fix that order: a stencil's border
|
|
1131
|
+
# is a second loop over the frame, a reduction may split its
|
|
1132
|
+
# accumulator, and the loop runs with the GVL released, which is not
|
|
1133
|
+
# where Ruby's Random -- the one `CArray#random!` draws through -- may
|
|
1134
|
+
# be reached at all. An array filled before the call has none of those
|
|
1135
|
+
# questions in it.
|
|
1136
|
+
def draw_message (what)
|
|
1137
|
+
"#{what} draws from a generator, and a kernel does not fix the order " \
|
|
1138
|
+
"it would draw in; fill an array with `CArray#random!` and read a " \
|
|
1139
|
+
"cell of it, as the kernel reads any other array"
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
# An extent is a Range, an Integer standing for `0...n`, or an
|
|
1143
|
+
# Enumerator::ArithmeticSequence -- which is what `(hi-1).step(lo, -1)`
|
|
1144
|
+
# returns, and how a downward loop is written.
|
|
1145
|
+
#
|
|
1146
|
+
# Returns the half-open pair per axis, and the direction the extent
|
|
1147
|
+
# asked for where it said one.
|
|
1148
|
+
def bounds (extents, rank)
|
|
1149
|
+
extents.map { |extent| bounds_of(extent) }
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
def bounds_of (extent)
|
|
1153
|
+
case extent
|
|
1154
|
+
when Range
|
|
1155
|
+
low = extent.begin || 0
|
|
1156
|
+
high = extent.end
|
|
1157
|
+
raise Unsupported, "an endless range has no extent" unless high
|
|
1158
|
+
[[low, extent.exclude_end? ? high : high + 1, 1], nil]
|
|
1159
|
+
when Integer
|
|
1160
|
+
[[0, extent, 1], nil]
|
|
1161
|
+
when Enumerator::ArithmeticSequence
|
|
1162
|
+
arithmetic_bounds(extent)
|
|
1163
|
+
else
|
|
1164
|
+
raise Unsupported,
|
|
1165
|
+
"an extent is a Range, an Integer or an arithmetic sequence, " \
|
|
1166
|
+
"got #{extent.class}"
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
# A sequence may skip cells. An offset still means what it means in
|
|
1171
|
+
# Ruby -- `a[i-1]` is the cell at index i-1 -- so skipping changes not
|
|
1172
|
+
# the reading but the dependencies: with a step of two, index i-1 is a
|
|
1173
|
+
# cell this loop never writes.
|
|
1174
|
+
def arithmetic_bounds (extent)
|
|
1175
|
+
step = extent.step
|
|
1176
|
+
raise Unsupported, "an extent's step cannot be zero" if step.zero?
|
|
1177
|
+
first = extent.begin
|
|
1178
|
+
last = extent.end
|
|
1179
|
+
raise Unsupported, "an endless sequence has no extent" if last.nil?
|
|
1180
|
+
limit = extent.exclude_end? ? last : last + (step.positive? ? 1 : -1)
|
|
1181
|
+
[[first, limit, step], step.positive? ? :ascending : :descending]
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
# The direction an extent asked for has to be the one the kernel's own
|
|
1185
|
+
# dependencies require -- and where they require one, the extent has to
|
|
1186
|
+
# say so, because that is the only place a reader sees it.
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
# ---------------------------------------------------------------------------
|
|
1194
|
+
# CArray asks about an expression it is about to compute; this answers.
|
|
1195
|
+
#
|
|
1196
|
+
# Registering here rather than being reached for means a program that only
|
|
1197
|
+
# writes `CArray.fuse { ... }` gets the compiled path from having this gem
|
|
1198
|
+
# installed, and the same answer without it.
|
|
1199
|
+
# ---------------------------------------------------------------------------
|
|
1200
|
+
|
|
1201
|
+
# The expression front end needs things CArray gained after 3.0.0 -- the
|
|
1202
|
+
# plan, the kernel bodies as text, the flags they were built with, and
|
|
1203
|
+
# somewhere to register. Without them the Prism front end above is the
|
|
1204
|
+
# whole of this gem, exactly as before.
|
|
1205
|
+
if CArray.respond_to?(:expression_evaluator) &&
|
|
1206
|
+
CArray.respond_to?(:__kernel_body__) &&
|
|
1207
|
+
CArray.const_defined?(:BUILD_FLAGS)
|
|
1208
|
+
require "carray/jit/expression"
|
|
1209
|
+
CArray.expression_evaluator = CArray::JIT::Expression.new
|
|
1210
|
+
end
|