2024-03-11 01:10:51 +00:00
|
|
|
//! SIMD (Single Instruction; Multiple Data) convenience functions.
|
|
|
|
//!
|
|
|
|
//! May offer a potential boost in performance on some targets by performing
|
2024-07-02 09:35:49 +00:00
|
|
|
//! the same operation on multiple elements at once.
|
2024-03-11 01:10:51 +00:00
|
|
|
//!
|
|
|
|
//! Some functions are known to not work on MIPS.
|
2022-03-27 08:28:44 +00:00
|
|
|
|
|
|
|
const std = @import("std");
|
|
|
|
const builtin = @import("builtin");
|
|
|
|
|
2023-12-19 21:21:03 +00:00
|
|
|
pub fn suggestVectorLengthForCpu(comptime T: type, comptime cpu: std.Target.Cpu) ?comptime_int {
|
SIMD size suggestions: suggestions code now compiles, added more
architectures
The idea behind this is using the register capabilities in safe amounts,
there is still some consideration to be done.
+ Fixed compile error using std.Target.<arch>.featureSetHas
+ X86 MMX and "3DNOW" 64 bits register usage considered for vector size
+ Added ARM Neon recommened usage of 128 bits (The size of the register)
+ Added AARCH64 Neon and SVE for 128 bits. SVE could use in theory up to
2048 bits, but found only evidence of functional 512 bits on a super
computer, decided on using 128 bits as a safety
+ Added Altivec recommendation of using the 128 bits long register
+ Using MIPS msa 2x64bits capabilities, usage of 64 bits registers for MDMX
systems, need testing on how using bigger values affect performance
+ Using V extension on RISC-V, which is extendable via instructions, decided on 128 bits
as a value to not use all registers
+ in SPARC the 64 bits registers are used, a max of 32 registers
are to be used for configurable simd instructions, decided on using
the size of the register, need testing on performance hit on using a
bigger sized register vector size
2022-07-17 07:20:35 +00:00
|
|
|
// This is guesswork, if you have better suggestions can add it or edit the current here
|
2022-10-03 16:34:30 +00:00
|
|
|
const element_bit_size = @max(8, std.math.ceilPowerOfTwo(u16, @bitSizeOf(T)) catch unreachable);
|
SIMD size suggestions: suggestions code now compiles, added more
architectures
The idea behind this is using the register capabilities in safe amounts,
there is still some consideration to be done.
+ Fixed compile error using std.Target.<arch>.featureSetHas
+ X86 MMX and "3DNOW" 64 bits register usage considered for vector size
+ Added ARM Neon recommened usage of 128 bits (The size of the register)
+ Added AARCH64 Neon and SVE for 128 bits. SVE could use in theory up to
2048 bits, but found only evidence of functional 512 bits on a super
computer, decided on using 128 bits as a safety
+ Added Altivec recommendation of using the 128 bits long register
+ Using MIPS msa 2x64bits capabilities, usage of 64 bits registers for MDMX
systems, need testing on how using bigger values affect performance
+ Using V extension on RISC-V, which is extendable via instructions, decided on 128 bits
as a value to not use all registers
+ in SPARC the 64 bits registers are used, a max of 32 registers
are to be used for configurable simd instructions, decided on using
the size of the register, need testing on performance hit on using a
bigger sized register vector size
2022-07-17 07:20:35 +00:00
|
|
|
const vector_bit_size: u16 = blk: {
|
|
|
|
if (cpu.arch.isX86()) {
|
2023-05-13 10:21:49 +00:00
|
|
|
if (T == bool and std.Target.x86.featureSetHas(cpu.features, .prefer_mask_registers)) return 64;
|
2023-10-21 23:30:45 +00:00
|
|
|
if (builtin.zig_backend != .stage2_x86_64 and std.Target.x86.featureSetHas(cpu.features, .avx512f) and !std.Target.x86.featureSetHasAny(cpu.features, .{ .prefer_256_bit, .prefer_128_bit })) break :blk 512;
|
SIMD size suggestions: suggestions code now compiles, added more
architectures
The idea behind this is using the register capabilities in safe amounts,
there is still some consideration to be done.
+ Fixed compile error using std.Target.<arch>.featureSetHas
+ X86 MMX and "3DNOW" 64 bits register usage considered for vector size
+ Added ARM Neon recommened usage of 128 bits (The size of the register)
+ Added AARCH64 Neon and SVE for 128 bits. SVE could use in theory up to
2048 bits, but found only evidence of functional 512 bits on a super
computer, decided on using 128 bits as a safety
+ Added Altivec recommendation of using the 128 bits long register
+ Using MIPS msa 2x64bits capabilities, usage of 64 bits registers for MDMX
systems, need testing on how using bigger values affect performance
+ Using V extension on RISC-V, which is extendable via instructions, decided on 128 bits
as a value to not use all registers
+ in SPARC the 64 bits registers are used, a max of 32 registers
are to be used for configurable simd instructions, decided on using
the size of the register, need testing on performance hit on using a
bigger sized register vector size
2022-07-17 07:20:35 +00:00
|
|
|
if (std.Target.x86.featureSetHasAny(cpu.features, .{ .prefer_256_bit, .avx2 }) and !std.Target.x86.featureSetHas(cpu.features, .prefer_128_bit)) break :blk 256;
|
|
|
|
if (std.Target.x86.featureSetHas(cpu.features, .sse)) break :blk 128;
|
|
|
|
if (std.Target.x86.featureSetHasAny(cpu.features, .{ .mmx, .@"3dnow" })) break :blk 64;
|
2024-07-30 00:55:49 +00:00
|
|
|
} else if (cpu.arch.isArmOrThumb()) {
|
SIMD size suggestions: suggestions code now compiles, added more
architectures
The idea behind this is using the register capabilities in safe amounts,
there is still some consideration to be done.
+ Fixed compile error using std.Target.<arch>.featureSetHas
+ X86 MMX and "3DNOW" 64 bits register usage considered for vector size
+ Added ARM Neon recommened usage of 128 bits (The size of the register)
+ Added AARCH64 Neon and SVE for 128 bits. SVE could use in theory up to
2048 bits, but found only evidence of functional 512 bits on a super
computer, decided on using 128 bits as a safety
+ Added Altivec recommendation of using the 128 bits long register
+ Using MIPS msa 2x64bits capabilities, usage of 64 bits registers for MDMX
systems, need testing on how using bigger values affect performance
+ Using V extension on RISC-V, which is extendable via instructions, decided on 128 bits
as a value to not use all registers
+ in SPARC the 64 bits registers are used, a max of 32 registers
are to be used for configurable simd instructions, decided on using
the size of the register, need testing on performance hit on using a
bigger sized register vector size
2022-07-17 07:20:35 +00:00
|
|
|
if (std.Target.arm.featureSetHas(cpu.features, .neon)) break :blk 128;
|
|
|
|
} else if (cpu.arch.isAARCH64()) {
|
|
|
|
// SVE allows up to 2048 bits in the specification, as of 2022 the most powerful machine has implemented 512-bit
|
|
|
|
// I think is safer to just be on 128 until is more common
|
|
|
|
// TODO: Check on this return when bigger values are more common
|
|
|
|
if (std.Target.aarch64.featureSetHas(cpu.features, .sve)) break :blk 128;
|
|
|
|
if (std.Target.aarch64.featureSetHas(cpu.features, .neon)) break :blk 128;
|
2024-07-29 22:59:50 +00:00
|
|
|
} else if (cpu.arch.isPowerPC()) {
|
SIMD size suggestions: suggestions code now compiles, added more
architectures
The idea behind this is using the register capabilities in safe amounts,
there is still some consideration to be done.
+ Fixed compile error using std.Target.<arch>.featureSetHas
+ X86 MMX and "3DNOW" 64 bits register usage considered for vector size
+ Added ARM Neon recommened usage of 128 bits (The size of the register)
+ Added AARCH64 Neon and SVE for 128 bits. SVE could use in theory up to
2048 bits, but found only evidence of functional 512 bits on a super
computer, decided on using 128 bits as a safety
+ Added Altivec recommendation of using the 128 bits long register
+ Using MIPS msa 2x64bits capabilities, usage of 64 bits registers for MDMX
systems, need testing on how using bigger values affect performance
+ Using V extension on RISC-V, which is extendable via instructions, decided on 128 bits
as a value to not use all registers
+ in SPARC the 64 bits registers are used, a max of 32 registers
are to be used for configurable simd instructions, decided on using
the size of the register, need testing on performance hit on using a
bigger sized register vector size
2022-07-17 07:20:35 +00:00
|
|
|
if (std.Target.powerpc.featureSetHas(cpu.features, .altivec)) break :blk 128;
|
|
|
|
} else if (cpu.arch.isMIPS()) {
|
|
|
|
if (std.Target.mips.featureSetHas(cpu.features, .msa)) break :blk 128;
|
|
|
|
// TODO: Test MIPS capability to handle bigger vectors
|
|
|
|
// In theory MDMX and by extension mips3d have 32 registers of 64 bits which can use in parallel
|
|
|
|
// for multiple processing, but I don't know what's optimal here, if using
|
|
|
|
// the 2048 bits or using just 64 per vector or something in between
|
|
|
|
if (std.Target.mips.featureSetHas(cpu.features, std.Target.mips.Feature.mips3d)) break :blk 64;
|
|
|
|
} else if (cpu.arch.isRISCV()) {
|
2024-07-02 09:35:49 +00:00
|
|
|
// In RISC-V Vector Registers are length agnostic so there's no good way to determine the best size.
|
|
|
|
// The usual vector length in most RISC-V cpus is 256 bits, however it can get to multiple kB.
|
|
|
|
if (std.Target.riscv.featureSetHas(cpu.features, .v)) {
|
|
|
|
var vec_bit_length: u32 = 256;
|
|
|
|
if (std.Target.riscv.featureSetHas(cpu.features, .zvl32b)) {
|
|
|
|
vec_bit_length = 32;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl64b)) {
|
|
|
|
vec_bit_length = 64;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl128b)) {
|
|
|
|
vec_bit_length = 128;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl256b)) {
|
|
|
|
vec_bit_length = 256;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl512b)) {
|
|
|
|
vec_bit_length = 512;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl1024b)) {
|
|
|
|
vec_bit_length = 1024;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl2048b)) {
|
|
|
|
vec_bit_length = 2048;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl4096b)) {
|
|
|
|
vec_bit_length = 4096;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl8192b)) {
|
|
|
|
vec_bit_length = 8192;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl16384b)) {
|
|
|
|
vec_bit_length = 16384;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl32768b)) {
|
|
|
|
vec_bit_length = 32768;
|
|
|
|
} else if (std.Target.riscv.featureSetHas(cpu.features, .zvl65536b)) {
|
|
|
|
vec_bit_length = 65536;
|
|
|
|
}
|
|
|
|
break :blk vec_bit_length;
|
|
|
|
}
|
SIMD size suggestions: suggestions code now compiles, added more
architectures
The idea behind this is using the register capabilities in safe amounts,
there is still some consideration to be done.
+ Fixed compile error using std.Target.<arch>.featureSetHas
+ X86 MMX and "3DNOW" 64 bits register usage considered for vector size
+ Added ARM Neon recommened usage of 128 bits (The size of the register)
+ Added AARCH64 Neon and SVE for 128 bits. SVE could use in theory up to
2048 bits, but found only evidence of functional 512 bits on a super
computer, decided on using 128 bits as a safety
+ Added Altivec recommendation of using the 128 bits long register
+ Using MIPS msa 2x64bits capabilities, usage of 64 bits registers for MDMX
systems, need testing on how using bigger values affect performance
+ Using V extension on RISC-V, which is extendable via instructions, decided on 128 bits
as a value to not use all registers
+ in SPARC the 64 bits registers are used, a max of 32 registers
are to be used for configurable simd instructions, decided on using
the size of the register, need testing on performance hit on using a
bigger sized register vector size
2022-07-17 07:20:35 +00:00
|
|
|
} else if (cpu.arch.isSPARC()) {
|
|
|
|
// TODO: Test Sparc capability to handle bigger vectors
|
|
|
|
// In theory Sparc have 32 registers of 64 bits which can use in parallel
|
|
|
|
// for multiple processing, but I don't know what's optimal here, if using
|
|
|
|
// the 2048 bits or using just 64 per vector or something in between
|
|
|
|
if (std.Target.sparc.featureSetHasAny(cpu.features, .{ .vis, .vis2, .vis3 })) break :blk 64;
|
2023-03-20 16:45:12 +00:00
|
|
|
} else if (cpu.arch.isWasm()) {
|
|
|
|
if (std.Target.wasm.featureSetHas(cpu.features, .simd128)) break :blk 128;
|
SIMD size suggestions: suggestions code now compiles, added more
architectures
The idea behind this is using the register capabilities in safe amounts,
there is still some consideration to be done.
+ Fixed compile error using std.Target.<arch>.featureSetHas
+ X86 MMX and "3DNOW" 64 bits register usage considered for vector size
+ Added ARM Neon recommened usage of 128 bits (The size of the register)
+ Added AARCH64 Neon and SVE for 128 bits. SVE could use in theory up to
2048 bits, but found only evidence of functional 512 bits on a super
computer, decided on using 128 bits as a safety
+ Added Altivec recommendation of using the 128 bits long register
+ Using MIPS msa 2x64bits capabilities, usage of 64 bits registers for MDMX
systems, need testing on how using bigger values affect performance
+ Using V extension on RISC-V, which is extendable via instructions, decided on 128 bits
as a value to not use all registers
+ in SPARC the 64 bits registers are used, a max of 32 registers
are to be used for configurable simd instructions, decided on using
the size of the register, need testing on performance hit on using a
bigger sized register vector size
2022-07-17 07:20:35 +00:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
if (vector_bit_size <= element_bit_size) return null;
|
2022-03-27 08:28:44 +00:00
|
|
|
|
SIMD size suggestions: suggestions code now compiles, added more
architectures
The idea behind this is using the register capabilities in safe amounts,
there is still some consideration to be done.
+ Fixed compile error using std.Target.<arch>.featureSetHas
+ X86 MMX and "3DNOW" 64 bits register usage considered for vector size
+ Added ARM Neon recommened usage of 128 bits (The size of the register)
+ Added AARCH64 Neon and SVE for 128 bits. SVE could use in theory up to
2048 bits, but found only evidence of functional 512 bits on a super
computer, decided on using 128 bits as a safety
+ Added Altivec recommendation of using the 128 bits long register
+ Using MIPS msa 2x64bits capabilities, usage of 64 bits registers for MDMX
systems, need testing on how using bigger values affect performance
+ Using V extension on RISC-V, which is extendable via instructions, decided on 128 bits
as a value to not use all registers
+ in SPARC the 64 bits registers are used, a max of 32 registers
are to be used for configurable simd instructions, decided on using
the size of the register, need testing on performance hit on using a
bigger sized register vector size
2022-07-17 07:20:35 +00:00
|
|
|
return @divExact(vector_bit_size, element_bit_size);
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
2023-12-19 21:21:03 +00:00
|
|
|
/// Suggests a target-dependant vector length for a given type, or null if scalars are recommended.
|
2022-03-27 08:28:44 +00:00
|
|
|
/// Not yet implemented for every CPU architecture.
|
2023-12-19 21:21:03 +00:00
|
|
|
pub fn suggestVectorLength(comptime T: type) ?comptime_int {
|
|
|
|
return suggestVectorLengthForCpu(T, builtin.cpu);
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
2023-12-19 21:21:03 +00:00
|
|
|
test "suggestVectorLengthForCpu works with signed and unsigned values" {
|
2022-09-07 12:22:30 +00:00
|
|
|
comptime var cpu = std.Target.Cpu.baseline(std.Target.Cpu.Arch.x86_64);
|
2023-06-15 07:14:16 +00:00
|
|
|
comptime cpu.features.addFeature(@intFromEnum(std.Target.x86.Feature.avx512f));
|
2023-10-21 23:30:45 +00:00
|
|
|
comptime cpu.features.populateDependencies(&std.Target.x86.all_features);
|
2023-12-19 21:21:03 +00:00
|
|
|
const expected_len: usize = switch (builtin.zig_backend) {
|
2023-10-21 23:30:45 +00:00
|
|
|
.stage2_x86_64 => 8,
|
|
|
|
else => 16,
|
|
|
|
};
|
2023-12-19 21:21:03 +00:00
|
|
|
const signed_integer_len = suggestVectorLengthForCpu(i32, cpu).?;
|
|
|
|
const unsigned_integer_len = suggestVectorLengthForCpu(u32, cpu).?;
|
|
|
|
try std.testing.expectEqual(expected_len, unsigned_integer_len);
|
|
|
|
try std.testing.expectEqual(expected_len, signed_integer_len);
|
2022-09-07 12:22:30 +00:00
|
|
|
}
|
|
|
|
|
2022-03-27 08:28:44 +00:00
|
|
|
fn vectorLength(comptime VectorType: type) comptime_int {
|
|
|
|
return switch (@typeInfo(VectorType)) {
|
2024-08-28 01:35:53 +00:00
|
|
|
.vector => |info| info.len,
|
|
|
|
.array => |info| info.len,
|
2022-03-27 08:28:44 +00:00
|
|
|
else => @compileError("Invalid type " ++ @typeName(VectorType)),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the smallest type of unsigned ints capable of indexing any element within the given vector type.
|
|
|
|
pub fn VectorIndex(comptime VectorType: type) type {
|
|
|
|
return std.math.IntFittingRange(0, vectorLength(VectorType) - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the smallest type of unsigned ints capable of holding the length of the given vector type.
|
|
|
|
pub fn VectorCount(comptime VectorType: type) type {
|
|
|
|
return std.math.IntFittingRange(0, vectorLength(VectorType));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a vector containing the first `len` integers in order from 0 to `len`-1.
|
|
|
|
/// For example, `iota(i32, 8)` will return a vector containing `.{0, 1, 2, 3, 4, 5, 6, 7}`.
|
2023-01-05 01:27:52 +00:00
|
|
|
pub inline fn iota(comptime T: type, comptime len: usize) @Vector(len, T) {
|
|
|
|
comptime {
|
|
|
|
var out: [len]T = undefined;
|
2023-02-18 16:02:57 +00:00
|
|
|
for (&out, 0..) |*element, i| {
|
2023-01-05 01:27:52 +00:00
|
|
|
element.* = switch (@typeInfo(T)) {
|
2024-08-28 01:35:53 +00:00
|
|
|
.int => @as(T, @intCast(i)),
|
|
|
|
.float => @as(T, @floatFromInt(i)),
|
2023-01-05 01:27:52 +00:00
|
|
|
else => @compileError("Can't use type " ++ @typeName(T) ++ " in iota."),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return @as(@Vector(len, T), out);
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a vector containing the same elements as the input, but repeated until the desired length is reached.
|
|
|
|
/// For example, `repeat(8, [_]u32{1, 2, 3})` will return a vector containing `.{1, 2, 3, 1, 2, 3, 1, 2}`.
|
2022-03-30 18:12:14 +00:00
|
|
|
pub fn repeat(comptime len: usize, vec: anytype) @Vector(len, std.meta.Child(@TypeOf(vec))) {
|
2022-03-27 08:28:44 +00:00
|
|
|
const Child = std.meta.Child(@TypeOf(vec));
|
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
return @shuffle(Child, vec, undefined, iota(i32, len) % @as(@Vector(len, i32), @splat(@intCast(vectorLength(@TypeOf(vec))))));
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a vector containing all elements of the first vector at the lower indices followed by all elements of the second vector
|
|
|
|
/// at the higher indices.
|
2022-03-30 18:12:14 +00:00
|
|
|
pub fn join(a: anytype, b: anytype) @Vector(vectorLength(@TypeOf(a)) + vectorLength(@TypeOf(b)), std.meta.Child(@TypeOf(a))) {
|
2022-03-27 08:28:44 +00:00
|
|
|
const Child = std.meta.Child(@TypeOf(a));
|
|
|
|
const a_len = vectorLength(@TypeOf(a));
|
|
|
|
const b_len = vectorLength(@TypeOf(b));
|
|
|
|
|
|
|
|
return @shuffle(Child, a, b, @as([a_len]i32, iota(i32, a_len)) ++ @as([b_len]i32, ~iota(i32, b_len)));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a vector whose elements alternates between those of each input vector.
|
|
|
|
/// For example, `interlace(.{[4]u32{11, 12, 13, 14}, [4]u32{21, 22, 23, 24}})` returns a vector containing `.{11, 21, 12, 22, 13, 23, 14, 24}`.
|
2022-03-30 18:12:14 +00:00
|
|
|
pub fn interlace(vecs: anytype) @Vector(vectorLength(@TypeOf(vecs[0])) * vecs.len, std.meta.Child(@TypeOf(vecs[0]))) {
|
2022-03-27 08:28:44 +00:00
|
|
|
// interlace doesn't work on MIPS, for some reason.
|
|
|
|
// Notes from earlier debug attempt:
|
|
|
|
// The indices are correct. The problem seems to be with the @shuffle builtin.
|
|
|
|
// On MIPS, the test that interlaces small_base gives { 0, 2, 0, 0, 64, 255, 248, 200, 0, 0 }.
|
|
|
|
// Calling this with two inputs seems to work fine, but I'll let the compile error trigger for all inputs, just to be safe.
|
|
|
|
comptime if (builtin.cpu.arch.isMIPS()) @compileError("TODO: Find out why interlace() doesn't work on MIPS");
|
|
|
|
|
|
|
|
const VecType = @TypeOf(vecs[0]);
|
|
|
|
const vecs_arr = @as([vecs.len]VecType, vecs);
|
|
|
|
const Child = std.meta.Child(@TypeOf(vecs_arr[0]));
|
|
|
|
|
|
|
|
if (vecs_arr.len == 1) return vecs_arr[0];
|
|
|
|
|
|
|
|
const a_vec_count = (1 + vecs_arr.len) >> 1;
|
|
|
|
const b_vec_count = vecs_arr.len >> 1;
|
|
|
|
|
2023-06-22 17:46:56 +00:00
|
|
|
const a = interlace(@as(*const [a_vec_count]VecType, @ptrCast(vecs_arr[0..a_vec_count])).*);
|
|
|
|
const b = interlace(@as(*const [b_vec_count]VecType, @ptrCast(vecs_arr[a_vec_count..])).*);
|
2022-03-27 08:28:44 +00:00
|
|
|
|
|
|
|
const a_len = vectorLength(@TypeOf(a));
|
|
|
|
const b_len = vectorLength(@TypeOf(b));
|
|
|
|
const len = a_len + b_len;
|
|
|
|
|
|
|
|
const indices = comptime blk: {
|
2023-07-06 17:48:42 +00:00
|
|
|
const Vi32 = @Vector(len, i32);
|
2022-03-27 08:28:44 +00:00
|
|
|
const count_up = iota(i32, len);
|
2023-07-06 17:48:42 +00:00
|
|
|
const cycle = @divFloor(count_up, @as(Vi32, @splat(@intCast(vecs_arr.len))));
|
|
|
|
const select_mask = repeat(len, join(@as(@Vector(a_vec_count, bool), @splat(true)), @as(@Vector(b_vec_count, bool), @splat(false))));
|
|
|
|
const a_indices = count_up - cycle * @as(Vi32, @splat(@intCast(b_vec_count)));
|
|
|
|
const b_indices = shiftElementsRight(count_up - cycle * @as(Vi32, @splat(@intCast(a_vec_count))), a_vec_count, 0);
|
2022-03-27 08:28:44 +00:00
|
|
|
break :blk @select(i32, select_mask, a_indices, ~b_indices);
|
|
|
|
};
|
|
|
|
|
|
|
|
return @shuffle(Child, a, b, indices);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The contents of `interlaced` is evenly split between vec_count vectors that are returned as an array. They "take turns",
|
2023-04-30 17:02:08 +00:00
|
|
|
/// receiving one element from `interlaced` at a time.
|
2022-03-27 08:28:44 +00:00
|
|
|
pub fn deinterlace(
|
|
|
|
comptime vec_count: usize,
|
|
|
|
interlaced: anytype,
|
2022-03-30 18:12:14 +00:00
|
|
|
) [vec_count]@Vector(
|
2022-03-27 08:28:44 +00:00
|
|
|
vectorLength(@TypeOf(interlaced)) / vec_count,
|
|
|
|
std.meta.Child(@TypeOf(interlaced)),
|
|
|
|
) {
|
|
|
|
const vec_len = vectorLength(@TypeOf(interlaced)) / vec_count;
|
|
|
|
const Child = std.meta.Child(@TypeOf(interlaced));
|
|
|
|
|
2022-03-30 18:12:14 +00:00
|
|
|
var out: [vec_count]@Vector(vec_len, Child) = undefined;
|
2022-03-27 08:28:44 +00:00
|
|
|
|
|
|
|
comptime var i: usize = 0; // for-loops don't work for this, apparently.
|
|
|
|
inline while (i < out.len) : (i += 1) {
|
2023-07-06 17:48:42 +00:00
|
|
|
const indices = comptime iota(i32, vec_len) * @as(@Vector(vec_len, i32), @splat(@intCast(vec_count))) + @as(@Vector(vec_len, i32), @splat(@intCast(i)));
|
2022-03-27 08:28:44 +00:00
|
|
|
out[i] = @shuffle(Child, interlaced, undefined, indices);
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn extract(
|
|
|
|
vec: anytype,
|
|
|
|
comptime first: VectorIndex(@TypeOf(vec)),
|
|
|
|
comptime count: VectorCount(@TypeOf(vec)),
|
2022-03-30 18:12:14 +00:00
|
|
|
) @Vector(count, std.meta.Child(@TypeOf(vec))) {
|
2022-03-27 08:28:44 +00:00
|
|
|
const Child = std.meta.Child(@TypeOf(vec));
|
|
|
|
const len = vectorLength(@TypeOf(vec));
|
|
|
|
|
2023-06-22 17:46:56 +00:00
|
|
|
std.debug.assert(@as(comptime_int, @intCast(first)) + @as(comptime_int, @intCast(count)) <= len);
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
return @shuffle(Child, vec, undefined, iota(i32, count) + @as(@Vector(count, i32), @splat(@intCast(first))));
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
test "vector patterns" {
|
2023-10-22 19:46:33 +00:00
|
|
|
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
|
|
|
|
|
2022-03-30 18:12:14 +00:00
|
|
|
const base = @Vector(4, u32){ 10, 20, 30, 40 };
|
|
|
|
const other_base = @Vector(4, u32){ 55, 66, 77, 88 };
|
|
|
|
|
|
|
|
const small_bases = [5]@Vector(2, u8){
|
|
|
|
@Vector(2, u8){ 0, 1 },
|
|
|
|
@Vector(2, u8){ 2, 3 },
|
|
|
|
@Vector(2, u8){ 4, 5 },
|
|
|
|
@Vector(2, u8){ 6, 7 },
|
|
|
|
@Vector(2, u8){ 8, 9 },
|
2022-03-27 08:28:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
try std.testing.expectEqual([6]u32{ 10, 20, 30, 40, 10, 20 }, repeat(6, base));
|
|
|
|
try std.testing.expectEqual([8]u32{ 10, 20, 30, 40, 55, 66, 77, 88 }, join(base, other_base));
|
|
|
|
try std.testing.expectEqual([2]u32{ 20, 30 }, extract(base, 1, 2));
|
|
|
|
|
|
|
|
if (comptime !builtin.cpu.arch.isMIPS()) {
|
|
|
|
try std.testing.expectEqual([8]u32{ 10, 55, 20, 66, 30, 77, 40, 88 }, interlace(.{ base, other_base }));
|
|
|
|
|
|
|
|
const small_braid = interlace(small_bases);
|
|
|
|
try std.testing.expectEqual([10]u8{ 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 }, small_braid);
|
|
|
|
try std.testing.expectEqual(small_bases, deinterlace(small_bases.len, small_braid));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-19 21:21:03 +00:00
|
|
|
/// Joins two vectors, shifts them leftwards (towards lower indices) and extracts the leftmost elements into a vector the length of a and b.
|
2022-03-27 08:28:44 +00:00
|
|
|
pub fn mergeShift(a: anytype, b: anytype, comptime shift: VectorCount(@TypeOf(a, b))) @TypeOf(a, b) {
|
|
|
|
const len = vectorLength(@TypeOf(a, b));
|
|
|
|
|
|
|
|
return extract(join(a, b), shift, len);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Elements are shifted rightwards (towards higher indices). New elements are added to the left, and the rightmost elements are cut off
|
2023-12-19 21:21:03 +00:00
|
|
|
/// so that the length of the vector stays the same.
|
2022-03-27 08:28:44 +00:00
|
|
|
pub fn shiftElementsRight(vec: anytype, comptime amount: VectorCount(@TypeOf(vec)), shift_in: std.meta.Child(@TypeOf(vec))) @TypeOf(vec) {
|
|
|
|
// It may be possible to implement shifts and rotates with a runtime-friendly slice of two joined vectors, as the length of the
|
|
|
|
// slice would be comptime-known. This would permit vector shifts and rotates by a non-comptime-known amount.
|
|
|
|
// However, I am unsure whether compiler optimizations would handle that well enough on all platforms.
|
2023-07-06 17:48:42 +00:00
|
|
|
const V = @TypeOf(vec);
|
|
|
|
const len = vectorLength(V);
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
return mergeShift(@as(V, @splat(shift_in)), vec, len - amount);
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Elements are shifted leftwards (towards lower indices). New elements are added to the right, and the leftmost elements are cut off
|
|
|
|
/// so that no elements with indices below 0 remain.
|
|
|
|
pub fn shiftElementsLeft(vec: anytype, comptime amount: VectorCount(@TypeOf(vec)), shift_in: std.meta.Child(@TypeOf(vec))) @TypeOf(vec) {
|
2023-07-06 17:48:42 +00:00
|
|
|
const V = @TypeOf(vec);
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
return mergeShift(vec, @as(V, @splat(shift_in)), amount);
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Elements are shifted leftwards (towards lower indices). Elements that leave to the left will reappear to the right in the same order.
|
|
|
|
pub fn rotateElementsLeft(vec: anytype, comptime amount: VectorCount(@TypeOf(vec))) @TypeOf(vec) {
|
|
|
|
return mergeShift(vec, vec, amount);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Elements are shifted rightwards (towards higher indices). Elements that leave to the right will reappear to the left in the same order.
|
|
|
|
pub fn rotateElementsRight(vec: anytype, comptime amount: VectorCount(@TypeOf(vec))) @TypeOf(vec) {
|
|
|
|
return rotateElementsLeft(vec, vectorLength(@TypeOf(vec)) - amount);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn reverseOrder(vec: anytype) @TypeOf(vec) {
|
|
|
|
const Child = std.meta.Child(@TypeOf(vec));
|
|
|
|
const len = vectorLength(@TypeOf(vec));
|
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
return @shuffle(Child, vec, undefined, @as(@Vector(len, i32), @splat(@as(i32, @intCast(len)) - 1)) - iota(i32, len));
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
test "vector shifting" {
|
2023-10-22 19:46:33 +00:00
|
|
|
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
|
|
|
|
|
2022-03-30 18:12:14 +00:00
|
|
|
const base = @Vector(4, u32){ 10, 20, 30, 40 };
|
2022-03-27 08:28:44 +00:00
|
|
|
|
|
|
|
try std.testing.expectEqual([4]u32{ 30, 40, 999, 999 }, shiftElementsLeft(base, 2, 999));
|
|
|
|
try std.testing.expectEqual([4]u32{ 999, 999, 10, 20 }, shiftElementsRight(base, 2, 999));
|
|
|
|
try std.testing.expectEqual([4]u32{ 20, 30, 40, 10 }, rotateElementsLeft(base, 1));
|
|
|
|
try std.testing.expectEqual([4]u32{ 40, 10, 20, 30 }, rotateElementsRight(base, 1));
|
|
|
|
try std.testing.expectEqual([4]u32{ 40, 30, 20, 10 }, reverseOrder(base));
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn firstTrue(vec: anytype) ?VectorIndex(@TypeOf(vec)) {
|
|
|
|
const len = vectorLength(@TypeOf(vec));
|
|
|
|
const IndexInt = VectorIndex(@TypeOf(vec));
|
|
|
|
|
|
|
|
if (!@reduce(.Or, vec)) {
|
|
|
|
return null;
|
|
|
|
}
|
2023-07-06 17:48:42 +00:00
|
|
|
const all_max: @Vector(len, IndexInt) = @splat(~@as(IndexInt, 0));
|
|
|
|
const indices = @select(IndexInt, vec, iota(IndexInt, len), all_max);
|
2022-03-27 08:28:44 +00:00
|
|
|
return @reduce(.Min, indices);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn lastTrue(vec: anytype) ?VectorIndex(@TypeOf(vec)) {
|
|
|
|
const len = vectorLength(@TypeOf(vec));
|
|
|
|
const IndexInt = VectorIndex(@TypeOf(vec));
|
|
|
|
|
|
|
|
if (!@reduce(.Or, vec)) {
|
|
|
|
return null;
|
|
|
|
}
|
2023-07-06 17:48:42 +00:00
|
|
|
|
|
|
|
const all_zeroes: @Vector(len, IndexInt) = @splat(0);
|
|
|
|
const indices = @select(IndexInt, vec, iota(IndexInt, len), all_zeroes);
|
2022-03-27 08:28:44 +00:00
|
|
|
return @reduce(.Max, indices);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn countTrues(vec: anytype) VectorCount(@TypeOf(vec)) {
|
|
|
|
const len = vectorLength(@TypeOf(vec));
|
|
|
|
const CountIntType = VectorCount(@TypeOf(vec));
|
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
const all_ones: @Vector(len, CountIntType) = @splat(1);
|
|
|
|
const all_zeroes: @Vector(len, CountIntType) = @splat(0);
|
|
|
|
|
|
|
|
const one_if_true = @select(CountIntType, vec, all_ones, all_zeroes);
|
2022-03-27 08:28:44 +00:00
|
|
|
return @reduce(.Add, one_if_true);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn firstIndexOfValue(vec: anytype, value: std.meta.Child(@TypeOf(vec))) ?VectorIndex(@TypeOf(vec)) {
|
2023-07-06 17:48:42 +00:00
|
|
|
const V = @TypeOf(vec);
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
return firstTrue(vec == @as(V, @splat(value)));
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn lastIndexOfValue(vec: anytype, value: std.meta.Child(@TypeOf(vec))) ?VectorIndex(@TypeOf(vec)) {
|
2023-07-06 17:48:42 +00:00
|
|
|
const V = @TypeOf(vec);
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
return lastTrue(vec == @as(V, @splat(value)));
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn countElementsWithValue(vec: anytype, value: std.meta.Child(@TypeOf(vec))) VectorCount(@TypeOf(vec)) {
|
2023-07-06 17:48:42 +00:00
|
|
|
const V = @TypeOf(vec);
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
return countTrues(vec == @as(V, @splat(value)));
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
test "vector searching" {
|
2023-10-22 19:46:33 +00:00
|
|
|
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
|
|
|
|
|
2022-03-30 18:12:14 +00:00
|
|
|
const base = @Vector(8, u32){ 6, 4, 7, 4, 4, 2, 3, 7 };
|
2022-03-27 08:28:44 +00:00
|
|
|
|
|
|
|
try std.testing.expectEqual(@as(?u3, 1), firstIndexOfValue(base, 4));
|
|
|
|
try std.testing.expectEqual(@as(?u3, 4), lastIndexOfValue(base, 4));
|
|
|
|
try std.testing.expectEqual(@as(?u3, null), lastIndexOfValue(base, 99));
|
|
|
|
try std.testing.expectEqual(@as(u4, 3), countElementsWithValue(base, 4));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Same as prefixScan, but with a user-provided, mathematically associative function.
|
|
|
|
pub fn prefixScanWithFunc(
|
|
|
|
comptime hop: isize,
|
|
|
|
vec: anytype,
|
2022-04-05 15:08:33 +00:00
|
|
|
/// The error type that `func` might return. Set this to `void` if `func` doesn't return an error union.
|
2022-03-27 08:28:44 +00:00
|
|
|
comptime ErrorType: type,
|
|
|
|
comptime func: fn (@TypeOf(vec), @TypeOf(vec)) if (ErrorType == void) @TypeOf(vec) else ErrorType!@TypeOf(vec),
|
|
|
|
/// When one operand of the operation performed by `func` is this value, the result must equal the other operand.
|
|
|
|
/// For example, this should be 0 for addition or 1 for multiplication.
|
|
|
|
comptime identity: std.meta.Child(@TypeOf(vec)),
|
|
|
|
) if (ErrorType == void) @TypeOf(vec) else ErrorType!@TypeOf(vec) {
|
|
|
|
// I haven't debugged this, but it might be a cousin of sorts to what's going on with interlace.
|
|
|
|
comptime if (builtin.cpu.arch.isMIPS()) @compileError("TODO: Find out why prefixScan doesn't work on MIPS");
|
|
|
|
|
|
|
|
const len = vectorLength(@TypeOf(vec));
|
|
|
|
|
|
|
|
if (hop == 0) @compileError("hop can not be 0; you'd be going nowhere forever!");
|
|
|
|
const abs_hop = if (hop < 0) -hop else hop;
|
|
|
|
|
|
|
|
var acc = vec;
|
|
|
|
comptime var i = 0;
|
|
|
|
inline while ((abs_hop << i) < len) : (i += 1) {
|
|
|
|
const shifted = if (hop < 0) shiftElementsLeft(acc, abs_hop << i, identity) else shiftElementsRight(acc, abs_hop << i, identity);
|
|
|
|
|
|
|
|
acc = if (ErrorType == void) func(acc, shifted) else try func(acc, shifted);
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a vector whose elements are the result of performing the specified operation on the corresponding
|
|
|
|
/// element of the input vector and every hop'th element that came before it (or after, if hop is negative).
|
|
|
|
/// Supports the same operations as the @reduce() builtin. Takes O(logN) to compute.
|
|
|
|
/// The scan is not linear, which may affect floating point errors. This may affect the determinism of
|
|
|
|
/// algorithms that use this function.
|
|
|
|
pub fn prefixScan(comptime op: std.builtin.ReduceOp, comptime hop: isize, vec: anytype) @TypeOf(vec) {
|
|
|
|
const VecType = @TypeOf(vec);
|
|
|
|
const Child = std.meta.Child(VecType);
|
|
|
|
|
|
|
|
const identity = comptime switch (@typeInfo(Child)) {
|
2024-08-28 01:35:53 +00:00
|
|
|
.bool => switch (op) {
|
2022-03-27 08:28:44 +00:00
|
|
|
.Or, .Xor => false,
|
|
|
|
.And => true,
|
|
|
|
else => @compileError("Invalid prefixScan operation " ++ @tagName(op) ++ " for vector of booleans."),
|
|
|
|
},
|
2024-08-28 01:35:53 +00:00
|
|
|
.int => switch (op) {
|
2022-03-27 08:28:44 +00:00
|
|
|
.Max => std.math.minInt(Child),
|
|
|
|
.Add, .Or, .Xor => 0,
|
|
|
|
.Mul => 1,
|
|
|
|
.And, .Min => std.math.maxInt(Child),
|
|
|
|
},
|
2024-08-28 01:35:53 +00:00
|
|
|
.float => switch (op) {
|
2022-03-27 08:28:44 +00:00
|
|
|
.Max => -std.math.inf(Child),
|
|
|
|
.Add => 0,
|
|
|
|
.Mul => 1,
|
|
|
|
.Min => std.math.inf(Child),
|
|
|
|
else => @compileError("Invalid prefixScan operation " ++ @tagName(op) ++ " for vector of floats."),
|
|
|
|
},
|
|
|
|
else => @compileError("Invalid type " ++ @typeName(VecType) ++ " for prefixScan."),
|
|
|
|
};
|
|
|
|
|
|
|
|
const fn_container = struct {
|
|
|
|
fn opFn(a: VecType, b: VecType) VecType {
|
|
|
|
return if (Child == bool) switch (op) {
|
2023-07-06 17:48:42 +00:00
|
|
|
.And => @select(bool, a, b, @as(VecType, @splat(false))),
|
|
|
|
.Or => @select(bool, a, @as(VecType, @splat(true)), b),
|
2022-03-27 08:28:44 +00:00
|
|
|
.Xor => a != b,
|
|
|
|
else => unreachable,
|
|
|
|
} else switch (op) {
|
|
|
|
.And => a & b,
|
|
|
|
.Or => a | b,
|
|
|
|
.Xor => a ^ b,
|
|
|
|
.Add => a + b,
|
|
|
|
.Mul => a * b,
|
2022-10-03 16:34:30 +00:00
|
|
|
.Min => @min(a, b),
|
|
|
|
.Max => @max(a, b),
|
2022-03-27 08:28:44 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return prefixScanWithFunc(hop, vec, void, fn_container.opFn, identity);
|
|
|
|
}
|
|
|
|
|
|
|
|
test "vector prefix scan" {
|
2023-10-27 05:40:14 +00:00
|
|
|
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
|
2023-10-22 19:46:33 +00:00
|
|
|
|
2022-03-27 08:28:44 +00:00
|
|
|
if (comptime builtin.cpu.arch.isMIPS()) {
|
|
|
|
return error.SkipZigTest;
|
|
|
|
}
|
|
|
|
|
2022-03-30 18:12:14 +00:00
|
|
|
const int_base = @Vector(4, i32){ 11, 23, 9, -21 };
|
|
|
|
const float_base = @Vector(4, f32){ 2, 0.5, -10, 6.54321 };
|
|
|
|
const bool_base = @Vector(4, bool){ true, false, true, false };
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2023-07-06 17:48:42 +00:00
|
|
|
const ones: @Vector(32, u8) = @splat(1);
|
|
|
|
|
|
|
|
try std.testing.expectEqual(iota(u8, 32) + ones, prefixScan(.Add, 1, ones));
|
2022-03-30 18:12:14 +00:00
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 3, 1, 1 }, prefixScan(.And, 1, int_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 31, 31, -1 }, prefixScan(.Or, 1, int_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 28, 21, -2 }, prefixScan(.Xor, 1, int_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 34, 43, 22 }, prefixScan(.Add, 1, int_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 253, 2277, -47817 }, prefixScan(.Mul, 1, int_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 11, 9, -21 }, prefixScan(.Min, 1, int_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 23, 23, 23 }, prefixScan(.Max, 1, int_base));
|
2022-03-27 08:28:44 +00:00
|
|
|
|
|
|
|
// Trying to predict all inaccuracies when adding and multiplying floats with prefixScans would be a mess, so we don't test those.
|
2022-03-30 18:12:14 +00:00
|
|
|
try std.testing.expectEqual(@Vector(4, f32){ 2, 0.5, -10, -10 }, prefixScan(.Min, 1, float_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, f32){ 2, 2, 2, 6.54321 }, prefixScan(.Max, 1, float_base));
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2022-03-30 18:12:14 +00:00
|
|
|
try std.testing.expectEqual(@Vector(4, bool){ true, true, false, false }, prefixScan(.Xor, 1, bool_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, bool){ true, true, true, true }, prefixScan(.Or, 1, bool_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, bool){ true, false, false, false }, prefixScan(.And, 1, bool_base));
|
2022-03-27 08:28:44 +00:00
|
|
|
|
2022-03-30 18:12:14 +00:00
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 23, 20, 2 }, prefixScan(.Add, 2, int_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 22, 11, -12, -21 }, prefixScan(.Add, -1, int_base));
|
|
|
|
try std.testing.expectEqual(@Vector(4, i32){ 11, 23, 9, -10 }, prefixScan(.Add, 3, int_base));
|
2022-03-27 08:28:44 +00:00
|
|
|
}
|