carray-jit 0.1.5 → 0.1.7

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.
@@ -9,8 +9,8 @@ class CArray
9
9
  # names the block used, and casts only where a type actually changes.
10
10
  # `CARRAY_JIT_DUMP=1` prints it.
11
11
  #
12
- # Every kernel has the same C signature, so one Fiddle::Function shape
13
- # serves all of them; the per-kernel detail is unpacked into named locals
12
+ # Every kernel has the same C signature, so one function shape serves
13
+ # all of them; the per-kernel detail is unpacked into named locals
14
14
  # at the top of the body, where it also reads better.
15
15
  class CGenerator
16
16
 
@@ -421,6 +421,7 @@ class CArray
421
421
  @uses_real_arg = false
422
422
  @uses_real_power_complex = false
423
423
  @clamp_types = []
424
+ @store_types = []
424
425
  @gamma_types = []
425
426
  @complex_helpers = []
426
427
  @uses_floor_divide = false
@@ -727,6 +728,7 @@ class CArray
727
728
  :real_arg => @uses_real_arg,
728
729
  :real_power_complex => @uses_real_power_complex,
729
730
  :clamp => @clamp_types.dup,
731
+ :store => @store_types.dup,
730
732
  :gamma => @gamma_types.dup,
731
733
  :complex => @complex_helpers.dup,
732
734
  :index_check => @uses_index_check,
@@ -839,6 +841,7 @@ class CArray
839
841
  @uses_real_arg ||= needs[:real_arg]
840
842
  @uses_real_power_complex ||= needs[:real_power_complex]
841
843
  @clamp_types |= needs[:clamp] || []
844
+ @store_types |= needs[:store] || []
842
845
  @gamma_types |= needs[:gamma] || []
843
846
  @complex_helpers |= needs[:complex] || []
844
847
  @uses_index_check ||= needs[:index_check]
@@ -1095,6 +1098,31 @@ class CArray
1095
1098
  C
1096
1099
  end
1097
1100
  text << intrinsic_helpers
1101
+ @store_types.each do |storage|
1102
+ low, high = STORE_BOUNDS.fetch(storage)
1103
+ c_type = storage_c_type(storage)
1104
+ text << <<~C
1105
+ /* A real number stored into an #{storage} cell keeps its integer
1106
+ part, as CArray's store does. One with no #{storage} to keep --
1107
+ a NaN, an infinity, a number past the range -- is refused as
1108
+ CArray refuses it: C leaves that conversion undefined, and the
1109
+ machines answer it differently. */
1110
+ static inline #{c_type}
1111
+ carray_jit_store_#{storage} (double value, int32_t *error)
1112
+ {
1113
+ if ( value > #{low} && value < #{high} ) {
1114
+ return (#{c_type}) value;
1115
+ }
1116
+ if ( error ) {
1117
+ *error = isnan(value) ? #{FLOAT_NAN_CODE} :
1118
+ isinf(value) ? ( value > 0 ? #{FLOAT_INFINITY_CODE} : #{FLOAT_NEGATIVE_INFINITY_CODE} ) :
1119
+ #{STORE_RANGE_CODES.fetch(storage)};
1120
+ }
1121
+ return 0;
1122
+ }
1123
+
1124
+ C
1125
+ end
1098
1126
  @clamp_types.each do |type|
1099
1127
  suffix, c_type, has_nan = CLAMP_C_TYPES.fetch(type)
1100
1128
  refusal = lambda { |test, code|
@@ -1749,6 +1777,14 @@ class CArray
1749
1777
  # Each axis carries a start, a limit and a step. The step is a compiled
1750
1778
  # constant when it is one, because `i++` is what lets the loop vectorise
1751
1779
  # and that is the case nearly every kernel is in.
1780
+ #
1781
+ # The limit and a step that is not a constant are read into locals
1782
+ # before the loop, not out of `bounds` at each test. `bounds` is an
1783
+ # `int64_t *`, and so is an int64 array the body writes: the compiler
1784
+ # cannot tell that a store into the array leaves the limit alone, reads
1785
+ # it again after every cell, and having no trip count does not vectorise
1786
+ # the loop at all. A kernel writing a float64 or int32 array never met
1787
+ # this, which is why it went unseen; test_vectorization.rb keeps it seen.
1752
1788
  def loop_open
1753
1789
  @rank.times.map { |axis|
1754
1790
  indent = " " + " " * axis
@@ -1760,17 +1796,18 @@ class CArray
1760
1796
  # way to report does not pay for the test at all.
1761
1797
  guard = @body_reports ? "#{indent} if ( *error ) break;\n" : ""
1762
1798
  start = "bounds[#{3 * axis}]"
1763
- limit = "bounds[#{3 * axis + 1}]"
1799
+ limit = "#{index}__limit"
1764
1800
  step = @steps[axis]
1765
- if step && step.negative?
1766
- advance = step == -1 ? "#{index}--" : "#{index} += bounds[#{3 * axis + 2}]"
1767
- "#{indent}for (int64_t #{index} = #{start}; #{index} > #{limit}; " \
1768
- "#{advance}) {\n#{guard}"
1801
+ locals = "#{index} = #{start}, #{limit} = bounds[#{3 * axis + 1}]"
1802
+ if step && step.abs == 1
1803
+ advance = step == -1 ? "#{index}--" : "#{index}++"
1769
1804
  else
1770
- advance = step == 1 ? "#{index}++" : "#{index} += bounds[#{3 * axis + 2}]"
1771
- "#{indent}for (int64_t #{index} = #{start}; #{index} < #{limit}; " \
1772
- "#{advance}) {\n#{guard}"
1805
+ locals += ", #{index}__step = bounds[#{3 * axis + 2}]"
1806
+ advance = "#{index} += #{index}__step"
1773
1807
  end
1808
+ comparison = ( step and step.negative? ) ? ">" : "<"
1809
+ "#{indent}for (int64_t #{locals}; #{index} #{comparison} #{limit}; " \
1810
+ "#{advance}) {\n#{guard}"
1774
1811
  }.join
1775
1812
  end
1776
1813
 
@@ -1834,6 +1871,14 @@ class CArray
1834
1871
  FLOAT_NEGATIVE_INFINITY_CODE = -5
1835
1872
  INTEGER_RANGE_CODE = -6
1836
1873
 
1874
+ # And what a real number stored into an integer cell reports where the
1875
+ # cell's type has no integer for it, one code for each type, so the
1876
+ # message can name the type as CArray's own store does.
1877
+ STORE_RANGE_CODES = {
1878
+ "int8" => -7, "int16" => -8, "int32" => -9, "int64" => -10,
1879
+ "uint8" => -11, "uint16" => -12, "uint32" => -13, "uint64" => -14,
1880
+ }.freeze
1881
+
1837
1882
  # The fixed failures a body reports, as the exception each one raises.
1838
1883
  # A kernel and a compiled function both look here, so the two say the
1839
1884
  # same thing for the same cell.
@@ -1842,6 +1887,24 @@ class CArray
1842
1887
  FLOAT_INFINITY_CODE => [FloatDomainError, "Infinity"],
1843
1888
  FLOAT_NEGATIVE_INFINITY_CODE => [FloatDomainError, "-Infinity"],
1844
1889
  INTEGER_RANGE_CODE => [RangeError, "a rounded Float is past the range of int64"],
1890
+ **STORE_RANGE_CODES.to_h { |storage, code|
1891
+ [code, [RangeError, "a Float is out of range for #{storage}"]]
1892
+ },
1893
+ }.freeze
1894
+
1895
+ # The bounds a real number has to fall strictly between for its integer
1896
+ # part to be a value of the type, written as doubles, which hold every
1897
+ # one of them exactly. CArray's store keeps the integer part, so -0.5
1898
+ # goes into a uint8 cell as 0, and 255.9 as 255.
1899
+ STORE_BOUNDS = {
1900
+ "int8" => ["-129.0", "128.0"],
1901
+ "int16" => ["-32769.0", "32768.0"],
1902
+ "int32" => ["-2147483649.0", "2147483648.0"],
1903
+ "int64" => ["-9223372036854777856.0", "9223372036854775808.0"],
1904
+ "uint8" => ["-1.0", "256.0"],
1905
+ "uint16" => ["-1.0", "65536.0"],
1906
+ "uint32" => ["-1.0", "4294967296.0"],
1907
+ "uint64" => ["-1.0", "18446744073709551616.0"],
1845
1908
  }.freeze
1846
1909
 
1847
1910
  RAISE_CODE_FLOOR = 3
@@ -2588,9 +2651,9 @@ class CArray
2588
2651
  # expression written into it carried, and the branches the write
2589
2652
  # stands in. A write reached because a missing cell said so was
2590
2653
  # reached for no reason, whichever of the three it writes.
2654
+ masked = @masked ? combine_masks(@carried_masks + [emit_mask(write.expression)]) : nil
2591
2655
  mask = if @masked
2592
- "#{indent}#{local_array_reference(write, :mask)} = " \
2593
- "#{combine_masks(@carried_masks + [emit_mask(write.expression)])};\n"
2656
+ "#{indent}#{local_array_reference(write, :mask)} = #{masked};\n"
2594
2657
  else
2595
2658
  ""
2596
2659
  end
@@ -2600,10 +2663,10 @@ class CArray
2600
2663
  "#{indent}#{COMPUTATION_C_TYPES.fetch(type)} #{temporary};\n" +
2601
2664
  emit_conditional_statement(expression, temporary, type, indent) +
2602
2665
  "#{indent}#{target} = " \
2603
- "#{cast_to_storage(temporary, type, write.storage)};\n" + mask
2666
+ "#{cast_to_storage(temporary, type, write.storage, masked: masked)};\n" + mask
2604
2667
  else
2605
2668
  type = complex_storage_of_power(expression, write.storage) || expression.type
2606
- value = cast_to_storage(emit(expression, type), type, write.storage)
2669
+ value = cast_to_storage(emit(expression, type), type, write.storage, masked: masked)
2607
2670
  "#{indent}#{target} = #{value};\n" + mask
2608
2671
  end
2609
2672
  end
@@ -2999,13 +3062,41 @@ class CArray
2999
3062
  @storage_types.fetch(array)
3000
3063
  end
3001
3064
 
3002
- def cast_to_storage (text, type, storage)
3065
+ # A captured array's element type is matched at the call
3066
+ # (`address_buffer`), after the build, and raises before the kernel
3067
+ # runs. Where the declaration spells the pointer differently from the
3068
+ # array's own, the address is cast to it, so that check is the one that
3069
+ # answers: gcc refuses an incompatible pointer at compile time where
3070
+ # clang only warns. `long long *` is such a spelling for an int64
3071
+ # array, which the check accepts and gcc does not. A body's own
3072
+ # pointer parameter is no captured array and is handed on as it is:
3073
+ # its type was declared in C.
3074
+ def emit_array_argument (argument, parameter)
3075
+ address = emit(argument, :address)
3076
+ return address unless @storage_types.key?(argument.array)
3077
+ own = "#{storage_c_type(array_storage(argument.array))} *"
3078
+ declared = parameter.decayed.sub(/\Aconst\s+/, "")
3079
+ declared == own ? address : "(#{parameter.decayed})#{address}"
3080
+ end
3081
+
3082
+ # `masked` is the C of the written cell's mask, for a write whose mask
3083
+ # the error slot does not already know about: a missing cell holds
3084
+ # whatever was under its mask, which is no value to refuse.
3085
+ def cast_to_storage (text, type, storage, masked: nil)
3003
3086
  # Storing into a boolean array normalises: CArray's boolean is a byte
3004
3087
  # holding 0 or 1, and a kernel is not the place that invariant stops
3005
3088
  # being true.
3006
3089
  return "(uint8_t)((#{text}) ? 1 : 0)" if boolean_storage?(storage)
3007
3090
  target = storage_c_type(storage)
3008
3091
  return text if COMPUTATION_C_TYPES.fetch(type) == target
3092
+ if [:double, :float].include?(type) and STORE_BOUNDS.key?(storage)
3093
+ @store_types |= [storage]
3094
+ error = error_argument
3095
+ if masked and masked != "0"
3096
+ error = "(#{masked} ? (int32_t *) 0 : #{error})"
3097
+ end
3098
+ return "carray_jit_store_#{storage}(#{text}, #{error})"
3099
+ end
3009
3100
  "(#{target})(#{text})"
3010
3101
  end
3011
3102
 
@@ -3563,8 +3654,9 @@ class CArray
3563
3654
  c_function = @c_functions.fetch(node.name)
3564
3655
  arguments = node.arguments.zip(c_function.parameters)
3565
3656
  .map { |argument, parameter|
3566
- if argument.is_a?(ArrayAddress) ||
3567
- argument.is_a?(LocalArrayAddress)
3657
+ if argument.is_a?(ArrayAddress)
3658
+ emit_array_argument(argument, parameter)
3659
+ elsif argument.is_a?(LocalArrayAddress)
3568
3660
  emit(argument, :address)
3569
3661
  else
3570
3662
  emit(argument, parameter.computation)
@@ -3623,12 +3715,15 @@ class CArray
3623
3715
  "#{emit(node.high, type)}, #{error_argument})", LEAF_PRECEDENCE]
3624
3716
  end
3625
3717
 
3626
- # `isnan` and `isfinite` are C's own, and take a number of any width.
3627
- # A Complex answers `finite?` the way Ruby answers it -- both parts
3628
- # finite -- through a helper, so that the number is computed once.
3629
- # An Integer is finite whatever it holds, and goes through `isfinite`
3630
- # all the same rather than being answered here: the read it stands for
3631
- # is a read, and dropping it would drop what the read reports.
3718
+ # `isnan` and `isfinite` are C's own, and take a real floating number
3719
+ # of either width. A Complex answers `finite?` the way Ruby answers
3720
+ # it -- both parts finite -- through a helper, so that the number is
3721
+ # computed once. An Integer is finite whatever it holds, and goes
3722
+ # through `isfinite` all the same rather than being answered here: the
3723
+ # read it stands for is a read, and dropping it would drop what the
3724
+ # read reports. It goes in as a double, because C asks for a floating
3725
+ # argument and gcc refuses an integer one where clang lets it pass;
3726
+ # every integer converts to a finite double.
3632
3727
  def emit_numeric_predicate (node)
3633
3728
  type = node.operand.type
3634
3729
  function = node.name == :nan? ? "isnan" : "isfinite"
@@ -3636,7 +3731,11 @@ class CArray
3636
3731
  return emit_helper_call("complex_finite", [emit(node.operand, type)],
3637
3732
  type == :float_complex)
3638
3733
  end
3639
- ["#{function}(#{emit(node.operand, type)})", LEAF_PRECEDENCE]
3734
+ operand = emit(node.operand, type)
3735
+ if TypeAssignment::INTEGER_TYPES.include?(type)
3736
+ operand = "(double)(#{operand})"
3737
+ end
3738
+ ["#{function}(#{operand})", LEAF_PRECEDENCE]
3640
3739
  end
3641
3740
 
3642
3741
  def emit_mask_test (node)
@@ -1,8 +1,9 @@
1
1
  require "digest"
2
2
  require "fileutils"
3
3
  require "rbconfig"
4
+ require "securerandom"
4
5
  require "tmpdir"
5
- require "fiddle"
6
+ require "carray/jit/native"
6
7
  require "monitor"
7
8
 
8
9
  class CArray
@@ -44,7 +45,7 @@ class CArray
44
45
 
45
46
  # Every generated object exports the same fixed names -- carray_jit_kernel,
46
47
  # carray_jit_slab, the border and error helpers -- because each one is a
47
- # module of its own. Fiddle.dlopen passes RTLD_GLOBAL, so on ELF those
48
+ # module of its own. Native opens each with RTLD_GLOBAL, so on ELF those
48
49
  # names all land in one flat namespace and an intra-module call resolves
49
50
  # to the first definition loaded: a later kernel's carray_jit_slab calls
50
51
  # an earlier kernel's carray_jit_kernel, which takes a different number
@@ -83,10 +84,11 @@ class CArray
83
84
  LINK = (RbConfig::CONFIG["host_os"] =~ /darwin/ ?
84
85
  ["-undefined", "dynamic_lookup"] : []).freeze
85
86
 
86
- # Kernels retained on disk. Each costs about 17 KB, so the default is
87
- # roughly 9 MB -- far more than any real program compiles, but a bound
88
- # all the same.
89
- DEFAULT_ENTRY_LIMIT = 512
87
+ # Kernels retained on disk. Each costs about 30 KB, object and source,
88
+ # so the default is roughly 60 MB. Rebuilding is what a bound too low
89
+ # costs -- on macOS a new object's first load is checked by the system,
90
+ # around 200 ms each -- and this gem's own suite wants some 1300.
91
+ DEFAULT_ENTRY_LIMIT = 2000
90
92
 
91
93
  class << self
92
94
 
@@ -223,7 +225,7 @@ class CArray
223
225
  removed
224
226
  end
225
227
 
226
- # Returns [Fiddle handle, whether it was compiled rather than reused].
228
+ # Returns [Native library, whether it was compiled rather than reused].
227
229
  #
228
230
  # `header` says where the kernel was written. It is written into the
229
231
  # .c file but kept out of the digest: two call sites that produce the
@@ -231,14 +233,16 @@ class CArray
231
233
  # kernel does not throw its object away. The file then names the
232
234
  # first site that compiled it, which is a place the code was written
233
235
  # 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.
236
+ # One compile at a time in a process, which spares a second thread the
237
+ # build: it takes the lock after the first has published, and finds
238
+ # the object where a later run would. It is not what keeps two builds
239
+ # apart -- each stages under a name of its own (#staging_name), so
240
+ # builds that meet anyway, as Ractors would, each publish a whole
241
+ # object. Until the names were that, two threads staged to one path
242
+ # and whichever renamed first left the other renaming a path that was
243
+ # gone -- a bare Errno::ENOENT out of three threads in four.
244
+ # Reentrant, so that a build reached from inside another -- which
245
+ # none is today -- would not stop here.
242
246
  LOCK = Monitor.new
243
247
 
244
248
  def build (source, function_name, header: nil, flags: FLAGS)
@@ -266,12 +270,17 @@ class CArray
266
270
  end
267
271
 
268
272
  source_path = File.join(directory, "#{key}.c")
269
- File.write(source_path, header.to_s + source)
270
- # Build to a unique path and rename, so a concurrent process never
271
- # loads a half-written object.
272
- staging_path = "#{object_path}.#{Process.pid}"
273
+ # Both written to a path of this build's own and renamed into
274
+ # place, so that nothing -- another build, another process -- ever
275
+ # reads half of either. A rename onto a path another build has
276
+ # just published replaces the same bytes.
277
+ staging = staging_name
278
+ staged_source = "#{source_path}.#{staging}"
279
+ staging_path = "#{object_path}.#{staging}"
273
280
  handle =
274
281
  begin
282
+ File.write(staged_source, header.to_s + source)
283
+ File.rename(staged_source, source_path)
275
284
  compile(source_path, staging_path, flags)
276
285
  # Opened before it is published, under the name only this
277
286
  # process knows. Published first, the object stands in the
@@ -291,6 +300,7 @@ class CArray
291
300
  # that cannot compile. The staging file would be swept in five
292
301
  # minutes; it goes now, beside the source it came from.
293
302
  remove_entry(object_path)
303
+ unlink(staged_source)
294
304
  unlink(staging_path)
295
305
  raise
296
306
  end
@@ -469,15 +479,23 @@ class CArray
469
479
  end
470
480
  end
471
481
 
472
- # A process killed mid-compile leaves `<key>.bundle.<pid>` behind.
473
- # Those match neither the entry glob nor the prune glob, so without
474
- # this they would sit in the cache for good. Only swept on a miss,
475
- # to keep it off the reuse path.
482
+ # The suffix one build stages its files under: the process, and a
483
+ # random part for the build within it. Not a counter, which would be
484
+ # state shared between the threads and Ractors that build at once.
485
+ def staging_name
486
+ "#{Process.pid}.#{SecureRandom.hex(4)}"
487
+ end
488
+
489
+ # A process killed mid-compile leaves `<key>.bundle.<staging>` behind,
490
+ # and `<key>.c.<staging>` if it was killed sooner. Those match neither
491
+ # the entry glob nor the prune glob, so without this they would sit in
492
+ # the cache for good. Only swept on a miss, to keep it off the reuse
493
+ # path.
476
494
  STAGING_MAXIMUM_AGE = 300
477
495
 
478
496
  def sweep_staging (directory)
479
497
  now = Time.now
480
- Dir[File.join(directory, "*#{shared_object_suffix}.*")].each do |path|
498
+ Dir[File.join(directory, "*{#{shared_object_suffix},.c}.*")].each do |path|
481
499
  next unless File.file?(path)
482
500
  age = now - File.mtime(path) rescue 0
483
501
  next if age < STAGING_MAXIMUM_AGE
@@ -572,8 +590,8 @@ class CArray
572
590
  # rebuilt. A freshly compiled object that will not load is a real
573
591
  # failure and is raised by #load_new_object.
574
592
  def load_shared_object (path)
575
- Fiddle.dlopen(path)
576
- rescue Fiddle::DLError
593
+ Native.open_library(path, global: true)
594
+ rescue Native::LinkError
577
595
  nil
578
596
  end
579
597
 
@@ -40,7 +40,17 @@ class CArray
40
40
  # multiply and an add into one rounding, so it compiles with
41
41
  # -ffp-contract=off. Here the reference is the eager kernel, which was
42
42
  # built with whatever CArray settled on.
43
- FLAGS = ["-fPIC", "-shared", *CArray::BUILD_FLAGS.split, *DYNAMIC_LOOKUP].freeze
43
+ #
44
+ # Except between nodes. CArray computes each node in a kernel of its
45
+ # own, so no node's multiply meets the next node's add; this object
46
+ # writes the expression as one statement per node in a single loop.
47
+ # clang contracts within a statement by default, which leaves that
48
+ # alone, but gcc defaults to -ffp-contract=fast and contracts across
49
+ # statements: on x86 with -march=native, `a * b + c` lost a rounding in
50
+ # 3296 of 30000 cells. -ffp-contract=on keeps a contraction inside a
51
+ # node's own body, as CArray's kernel has it, and none between nodes.
52
+ FLAGS = ["-fPIC", "-shared", *CArray::BUILD_FLAGS.split,
53
+ "-ffp-contract=on", *DYNAMIC_LOOKUP].freeze
44
54
 
45
55
  C_TYPES = {
46
56
  float64: "double", float32: "float",
@@ -73,9 +83,11 @@ class CArray
73
83
  # Held here so that the buffers outlive the call.
74
84
  dim = out.dim.pack("q*")
75
85
  steps = strided.flat_map { |slot| slot_strides(bases, slot) }.pack("q*")
86
+ constants = packed_constants(plan) or return false
76
87
  args = [out.elements]
77
- args << Fiddle::Pointer[dim] if walks_axes?(plan, strided)
78
- args << Fiddle::Pointer[steps] unless strided.empty?
88
+ args << dim if walks_axes?(plan, strided)
89
+ args << steps unless strided.empty?
90
+ args << constants unless constants.empty?
79
91
  kernel.call(*args, *pointers(plan, bases))
80
92
  end
81
93
  true
@@ -117,10 +129,9 @@ class CArray
117
129
  @strided = strided
118
130
  source = source_for(plan, aliased) or return nil
119
131
  handle, = Compiler.build(source, "carray_jit_expression", flags: FLAGS)
120
- Fiddle::Function.new(handle["carray_jit_expression"],
121
- [Fiddle::TYPE_LONG_LONG] +
122
- [Fiddle::TYPE_VOIDP] * (1 + arity(plan, strided)),
123
- Fiddle::TYPE_VOID)
132
+ Native.function(Native.address(handle, "carray_jit_expression"),
133
+ [:long_long] + [:pointer] * (1 + arity(plan, strided)),
134
+ :void, release_gvl: true)
124
135
  rescue CompilationError
125
136
  nil
126
137
  end
@@ -128,7 +139,63 @@ class CArray
128
139
  def arity (plan, strided)
129
140
  plan.leaves.size + plan.leaves.count { |array| array.has_mask? } +
130
141
  (plan.masked ? 1 : 0) + (walks_axes?(plan, strided) ? 1 : 0) +
131
- (strided.empty? ? 0 : 1)
142
+ (strided.empty? ? 0 : 1) + (constant_nodes(plan).empty? ? 0 : 1)
143
+ end
144
+
145
+ # The values the kernel does not have written into it: each constant,
146
+ # and the fill of each shifted read that fills. They arrive at the call
147
+ # instead. Written into the C as literals, they were the compiler's to
148
+ # fold, and it folded what CArray computes as arithmetic: `x * -1.0`
149
+ # became a negation, which flips the sign of a NaN that the multiply
150
+ # in CArray's own kernel leaves as it is. A value the compiler cannot
151
+ # see is one it computes with as written.
152
+ def constant_nodes (plan)
153
+ plan.nodes.each_with_index.filter_map { |node, i|
154
+ case node
155
+ when CArray::Fusion::Const then [i, node]
156
+ when CArray::Fusion::Shifted
157
+ [i, CArray::Fusion::Const.new(node.fill, node.data_type)] if node.bounds.uniq == [:fill]
158
+ end
159
+ }
160
+ end
161
+
162
+ # One slot of CONSTANT_SLOT bytes per value, in the order of
163
+ # #constant_nodes, each read back with memcpy so that no slot need be
164
+ # aligned. Nil for a value that has no bytes in its type -- the plan is
165
+ # then declined.
166
+ def packed_constants (plan)
167
+ constant_nodes(plan).map { |_, node| constant_bytes(node) or return nil }.join
168
+ end
169
+
170
+ CONSTANT_SLOT = 8
171
+
172
+ PACKING = {
173
+ float64: "d", float32: "f",
174
+ int8: "c", int16: "s", int32: "l", int64: "q",
175
+ uint8: "C", uint16: "S", uint32: "L", uint64: "Q",
176
+ boolean: "C",
177
+ }.freeze
178
+
179
+ def constant_bytes (node)
180
+ directive = PACKING[node.data_type] or return nil
181
+ value = node.value
182
+ value = case node.data_type
183
+ when :float64, :float32 then Float(value)
184
+ when :boolean then truth(value) ? 1 : 0
185
+ else value.is_a?(Float) ? (value.finite? ? value.to_i : (return nil)) : Integer(value)
186
+ end
187
+ [value].pack(directive).ljust(CONSTANT_SLOT, "\0")
188
+ rescue TypeError, ArgumentError, RangeError
189
+ nil
190
+ end
191
+
192
+ # The constants read into locals before the loop, where they stay for
193
+ # every cell.
194
+ def constant_lines (plan)
195
+ constant_nodes(plan).each_with_index.map { |(i, node), j|
196
+ type = C_TYPES.fetch(node.data_type)
197
+ " #{type} c#{i}; memcpy(&c#{i}, constants + #{j * CONSTANT_SLOT}, sizeof c#{i});"
198
+ }
132
199
  end
133
200
 
134
201
  # A plan that reads an operand shifted, or by strides, needs to know
@@ -246,9 +313,10 @@ class CArray
246
313
 
247
314
  #{helpers}
248
315
  void
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)})
316
+ carray_jit_expression (int64_t elements#{walking ? ", const int64_t *dim" : ""}#{@strided.empty? ? "" : ", const int64_t *steps"}#{constant_nodes(plan).empty? ? "" : ", const unsigned char *constants"}, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
250
317
  {
251
318
  #{stride_lines(plan).join("\n")}
319
+ #{constant_lines(plan).join("\n")}
252
320
  #{loop(plan, body + result, walking && (statements(plan, :inside) + result)).join("\n")}
253
321
  }
254
322
  C
@@ -351,7 +419,7 @@ class CArray
351
419
  when CArray::Fusion::Shifted
352
420
  shifted_line(plan, node, i, type)
353
421
  when CArray::Fusion::Const
354
- ["#{type} v#{i} = #{literal(node)};",
422
+ ["#{type} v#{i} = c#{i};",
355
423
  *(plan.masked ? ["uint8_t m#{i} = 0;"] : [])]
356
424
  when CArray::Fusion::Op
357
425
  statement = substitute(node, i, type) or return nil
@@ -387,8 +455,7 @@ class CArray
387
455
  clamped = (0...ndim).map { |k| "c#{i}_#{k}" }
388
456
  lines << "#{type} v#{i} = #{cell("a#{node.index}", type, flat, clamped)};"
389
457
  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)}; }"
458
+ lines << "if ( ! s#{i}_in ) { v#{i} = c#{i}; }"
392
459
  end
393
460
  if plan.masked
394
461
  inner = node.masked ? cell("k#{node.index}", "uint8_t", flat, clamped) : "0"
@@ -398,23 +465,6 @@ class CArray
398
465
  lines
399
466
  end
400
467
 
401
- def literal (node)
402
- case node.data_type
403
- when :float64, :float32 then float_literal(node.value.to_f)
404
- when :boolean then truth(node.value) ? 1 : 0
405
- else node.value.to_s
406
- end
407
- end
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
468
  # A boolean cell as CArray stores it: 0 is false, as is false.
419
469
  def truth (value)
420
470
  value.is_a?(Numeric) ? value != 0 : !!value