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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/README.md +2 -2
- data/carray-jit.gemspec +4 -2
- data/docs/02_KernelShapes.md +2 -2
- data/docs/03_SupportedFeatures.md +2 -2
- data/docs/04_Compiling.md +5 -5
- data/docs/08_LanguageReference.md +2 -2
- data/lib/carray/jit/analyzer.rb +1 -1
- data/lib/carray/jit/c_function.rb +140 -139
- data/lib/carray/jit/c_generator.rb +123 -24
- data/lib/carray/jit/compiler.rb +44 -26
- data/lib/carray/jit/expression.rb +79 -29
- data/lib/carray/jit/kernel.rb +23 -47
- data/lib/carray/jit/native.rb +149 -0
- data/lib/carray/jit/sweep.rb +14 -64
- data/lib/carray/jit/version.rb +1 -1
- data/lib/carray/jit.rb +96 -19
- metadata +7 -6
|
@@ -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
|
|
13
|
-
#
|
|
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 = "
|
|
1799
|
+
limit = "#{index}__limit"
|
|
1764
1800
|
step = @steps[axis]
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
1771
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3627
|
-
# A Complex answers `finite?` the way Ruby answers
|
|
3628
|
-
# finite -- through a helper, so that the number is
|
|
3629
|
-
# An Integer is finite whatever it holds, and goes
|
|
3630
|
-
# all the same rather than being answered here: the
|
|
3631
|
-
# is a read, and dropping it would drop what the
|
|
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
|
-
|
|
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)
|
data/lib/carray/jit/compiler.rb
CHANGED
|
@@ -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 "
|
|
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.
|
|
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
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
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 [
|
|
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
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
#
|
|
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
|
-
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
|
|
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
|
-
#
|
|
473
|
-
#
|
|
474
|
-
#
|
|
475
|
-
|
|
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, "
|
|
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
|
-
|
|
576
|
-
rescue
|
|
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
|
-
|
|
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 <<
|
|
78
|
-
args <<
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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} = #{
|
|
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
|
-
|
|
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
|