diff --git a/docs/src/usage.md b/docs/src/usage.md index 4081e6f..4b06116 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -24,12 +24,12 @@ julia> arr = [10 20; 30 14] 30 14 julia> FixedSizeArray(arr) # construct from an `AbstractArray` value -2×2 FixedSizeArray{Int64, 2, Memory{Int64}}: +2×2 FixedSizeArray{Int64, 2, FixedSizeArrays.UnsafeRefArray{Int64}}: 10 20 30 14 julia> FixedSizeArray{Float64}(arr) # construct from an `AbstractArray` value while converting element type -2×2 FixedSizeArray{Float64, 2, Memory{Float64}}: +2×2 FixedSizeArray{Float64, 2, FixedSizeArrays.UnsafeRefArray{Float64}}: 10.0 20.0 30.0 14.0 ``` @@ -45,7 +45,7 @@ julia> arr = transpose([10 20; 30 14]) 20 14 julia> adapt(FixedSizeArray, arr) -2×2 transpose(::FixedSizeArray{Int64, 2, Memory{Int64}}) with eltype Int64: +2×2 transpose(::FixedSizeArray{Int64, 2, FixedSizeArrays.UnsafeRefArray{Int64}}) with eltype Int64: 10 30 20 14 ``` @@ -68,7 +68,7 @@ The `FixedSizeArray{T,N,Mem}` type has three parameters: !!! note "Implementation details" - In Julia v1.11+, the default memory backend is the [`Memory{T}`](https://docs.julialang.org/en/v1/base/arrays/#Core.Memory) type. + In Julia v1.11+, the default memory backend is `UnsafeRefArray{T}`, an internal type that wraps a `MemoryRef{T}`. This allows the pointer to be inlined into the `FixedSizeArray` struct, saving a memory indirection compared to `Memory{T}`. Since Julia v1.10 does not have the `Memory` type, to make this package usable also on Julia v1.10 `Vector{T}` is used as memory backend, but many of the memory/performance optimizations enabled by this package will not be available in that version of Julia and in general `FixedSizeArrays.jl` does not provide significant improvements compared to `Base`'s `Array` for that specific version. To make it easier to refer to the concrete type `FixedSizeArray{T,N,Mem}` with the default memory backend, the following convenient aliases are available: @@ -88,17 +88,17 @@ julia> iter = (i for i ∈ 7:9 if i≠8); julia> using FixedSizeArrays, Collects julia> collect_as(FixedSizeArray, iter) # construct from an arbitrary iterator -2-element FixedSizeArray{Int64, 1, Memory{Int64}}: +2-element FixedSizeArray{Int64, 1, FixedSizeArrays.UnsafeRefArray{Int64}}: 7 9 julia> collect_as(FixedSizeArray{Float64}, iter) # construct from an arbitrary iterator while converting element type -2-element FixedSizeArray{Float64, 1, Memory{Float64}}: +2-element FixedSizeArray{Float64, 1, FixedSizeArrays.UnsafeRefArray{Float64}}: 7.0 9.0 julia> collect_as(FixedSizeVectorDefault, (3.14, -4.2, 2.68)) # construct from a tuple -3-element FixedSizeArray{Float64, 1, Memory{Float64}}: +3-element FixedSizeArray{Float64, 1, FixedSizeArrays.UnsafeRefArray{Float64}}: 3.14 -4.2 2.68 diff --git a/ext/FixedSizeArraysRandomExt.jl b/ext/FixedSizeArraysRandomExt.jl index de1d07f..051c8a9 100644 --- a/ext/FixedSizeArraysRandomExt.jl +++ b/ext/FixedSizeArraysRandomExt.jl @@ -21,4 +21,20 @@ if VERSION < v"1.11" end end +# UnsafeRefArray-backed: delegate rand! to the underlying Memory so that the +# specialized SIMD path in Random.XoshiroSimd is hit. +if @isdefined Memory + using FixedSizeArrays: UnsafeRefArray + + function Random.rand!(rng::Random.AbstractRNG, A::UnsafeRefArray{T}, sp::Random.Sampler) where {T} + Random.rand!(rng, parent(A), sp) + return A + end + + function Random.rand!(r::Random.MersenneTwister, A::UnsafeRefArray{Float64}, I::Random.SamplerTrivial{<:Random.FloatInterval{Float64}}) + Random.rand!(r, parent(A), I) + return A + end +end + end # module FixedSizeArraysRandomExt diff --git a/src/FixedSizeArray.jl b/src/FixedSizeArray.jl index 0a621f7..9185a41 100644 --- a/src/FixedSizeArray.jl +++ b/src/FixedSizeArray.jl @@ -253,6 +253,12 @@ function with_stripped_type_parameters_unchecked(::TypeParametersElementType, :: s = GenericMemory{K, T, AS} where {T} Val{s}() end +if (@isdefined UnsafeRefArray) + function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Type{<:UnsafeRefArray}) + s = UnsafeRefArray + Val{s}() + end +end # `Base.@assume_effects :consistent` is a workaround for: # https://github.com/JuliaLang/julia/issues/56966 @@ -418,7 +424,7 @@ end FixedSizeArrayDefault{T,N}(undef, size1::Int, size2::Int, ...) FixedSizeArrayDefault{T,N}(array::AbstractArray) -Construct a [`FixedSizeArray`](@ref) with element type `T`, number of dimensions `N`, and the default memory backend (`Vector{T}` on Julia v1.10, `Memory{T}` on Julia v1.11+). +Construct a [`FixedSizeArray`](@ref) with element type `T`, number of dimensions `N`, and the default memory backend (`Vector{T}` on Julia v1.10, `UnsafeRefArray{T}` on Julia v1.11+). """ const FixedSizeArrayDefault = FixedSizeArray{T, N, default_underlying_storage_type{T}} where {T, N} """ @@ -426,7 +432,7 @@ const FixedSizeArrayDefault = FixedSizeArray{T, N, default_underlying_storage_ty FixedSizeVectorDefault{T}(undef, size1::Int) FixedSizeVectorDefault{T}(array::AbstractVector) -Construct a [`FixedSizeVector`](@ref) with element type `T`, and the default memory backend (`Vector{T}` on Julia v1.10, `Memory{T}` on Julia v1.11+). +Construct a [`FixedSizeVector`](@ref) with element type `T`, and the default memory backend (`Vector{T}` on Julia v1.10, `UnsafeRefArray{T}` on Julia v1.11+). """ const FixedSizeVectorDefault = FixedSizeArrayDefault{T, 1} where {T} """ @@ -434,6 +440,6 @@ const FixedSizeVectorDefault = FixedSizeArrayDefault{T, 1} where {T} FixedSizeMatrixDefault{T}(undef, size1::Int, size2::Int) FixedSizeMatrixDefault{T}(array::AbstractMatrix) -Construct a [`FixedSizeMatrix`](@ref) with element type `T`, and the default memory backend (`Vector{T}` on Julia v1.10, `Memory{T}` on Julia v1.11+). +Construct a [`FixedSizeMatrix`](@ref) with element type `T`, and the default memory backend (`Vector{T}` on Julia v1.10, `UnsafeRefArray{T}` on Julia v1.11+). """ const FixedSizeMatrixDefault = FixedSizeArrayDefault{T, 2} where {T} diff --git a/src/FixedSizeArrays.jl b/src/FixedSizeArrays.jl index 5349208..af58c2c 100644 --- a/src/FixedSizeArrays.jl +++ b/src/FixedSizeArrays.jl @@ -5,11 +5,15 @@ using Collects: Collect, collect_as using LightBoundsErrors: checkbounds_lightboundserror using LightBoundsErrors: LightBoundsError as BoundsErrorLight -const default_underlying_storage_type = (@isdefined Memory) ? Memory : Vector +if (@isdefined Memory) + include("refarray.jl") +end + +const default_underlying_storage_type = (@isdefined Memory) ? UnsafeRefArray : Vector -const optional_memory = (@isdefined Memory) ? (Memory,) : () +const optional_memory = (@isdefined Memory) ? (UnsafeRefArray,) : () const optional_atomic_memory = (@isdefined AtomicMemory) ? (AtomicMemory,) : () -const optional_generic_memory = (@isdefined GenericMemory) ? (GenericMemory,) : () +const optional_generic_memory = (@isdefined GenericMemory) ? (UnsafeRefArray,) : () include("FixedSizeArray.jl") diff --git a/src/refarray.jl b/src/refarray.jl new file mode 100644 index 0000000..f8814f2 --- /dev/null +++ b/src/refarray.jl @@ -0,0 +1,102 @@ +# Internal type used as the default, heap-backed parent container for FixedSizeArray. +# This is used over Memory{T}, because a MemoryRef inlines into the parent struct, and +# contains the pointer. This way, it saves a memory indirection over Memory. +# This type does not boundscheck, hence the unsafety. The FSA wrapper will handle boundschecks. +struct UnsafeRefArray{T} <: DenseVector{T} + ref::MemoryRef{T} +end + +# TODO: Would be nice to be able to omit the check for n < 0 in the Memory +# constructor, but this is in the memorynew built-in and cannot be disabled +function UnsafeRefArray{T}(::UndefInitializer, n::Int) where T + mem = Memory{T}(undef, n) + UnsafeRefArray{T}(memoryref(mem)) +end + +UnsafeRefArray{T}(::UndefInitializer, n::Tuple{Int}) where T = UnsafeRefArray{T}(undef, @inbounds(n[1])) + +@static if VERSION < v"1.12.0-DEV.966" + Base.parent(x::UnsafeRefArray) = x.ref.mem +else + Base.parent(x::UnsafeRefArray) = parent(x.ref) +end + +# This version introduced Base.memoryindex as public API +@static if VERSION < v"1.13.0-DEV.1289" + internal_memindex(x::MemoryRef) = Core.memoryrefoffset(x) +else + internal_memindex(x::MemoryRef) = Base.memoryindex(x) +end + +function Base.checkbounds(A::UnsafeRefArray, is...) + checkbounds_lightboundserror(A, is...) +end + +Base.size(x::UnsafeRefArray) = ((length(parent(x)) - internal_memindex(x.ref) + 1),) + +Base.firstindex(::UnsafeRefArray) = 1 + +function Base.getindex(x::UnsafeRefArray, i::Int) + @inbounds(memoryref(x.ref, i)[]) +end + +function Base.setindex!(x::UnsafeRefArray{T}, v, i::Int) where T + vT = convert(T, v)::T + @inbounds(memoryref(x.ref, i)[] = vT) +end + +Base.IndexStyle(::Type{<:UnsafeRefArray}) = Base.IndexLinear() + +Base.elsize(::Type{UnsafeRefArray{T}}) where {T} = Base.elsize(Memory{T}) + +function Base.similar(A::UnsafeRefArray, ::Type{S}, dims::Dims) where S + UnsafeRefArray{S}(undef, dims) +end + +function Base.isassigned(x::UnsafeRefArray, i::Int) + @boundscheck checkbounds(Bool, x, i) || false + ref = @inbounds memoryref(x.ref, i) + isassigned(ref) +end + +function Base.copy(x::UnsafeRefArray) + len = length(x) + newmem = Memory{eltype(x)}(undef, len) + newref = memoryref(newmem) + @inbounds unsafe_copyto!(newref, x.ref, len) + typeof(x)(newref) +end + +function Base.unsafe_convert(::Type{Ptr{T}}, x::UnsafeRefArray) where T + Base.unsafe_convert(Ptr{T}, x.ref) +end + +Base.dataids(x::UnsafeRefArray) = Base.dataids(parent(x)) + +# Collects.jl interface for UnsafeRefArray + +function (c::Collect)(::Type{UnsafeRefArray{T}}, collection) where {T} + if collection isa UnsafeRefArray{T} + return copy(collection) + end + if (T isa Type) && Base.IteratorSize(collection) isa Union{Base.HasLength, Base.HasShape} + result = UnsafeRefArray{T}(undef, Int(length(collection))::Int) + copyto!(result, collection) + return result + end + vec = c(Vector{T}, collection) + result = UnsafeRefArray{T}(undef, length(vec)) + copyto!(result, vec) + result +end + +function (c::Collect)(::Type{UnsafeRefArray}, collection) + if collection isa UnsafeRefArray + return copy(collection) + end + vec = c(Vector, collection) + T = eltype(vec) + result = UnsafeRefArray{T}(undef, length(vec)) + copyto!(result, vec) + result +end diff --git a/test/runtests.jl b/test/runtests.jl index 198cc62..c10712c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,9 @@ using Test using LinearAlgebra, Adapt using FixedSizeArrays using FixedSizeArrays: checked_dims +if @isdefined Memory + using FixedSizeArrays: UnsafeRefArray +end using Collects: collect_as using OffsetArrays: OffsetArray using Random: Random @@ -137,7 +140,7 @@ end @testset "default underlying storage type" begin default = FixedSizeArrays.default_underlying_storage_type - @test default === (@isdefined(Memory) ? Memory : Vector) + @test default === (@isdefined(Memory) ? UnsafeRefArray : Vector) return_type = FixedSizeVector{Int,default{Int}} @test return_type === FixedSizeVectorDefault{Int} test_inferred(FixedSizeArray{Int}, return_type, (undef, 3)) @@ -155,6 +158,31 @@ end test_inferred(FixedSizeVector{Int}, return_type, arr) end + (@isdefined Memory) && @testset "UnsafeRefArray" begin + @testset "tuple undef constructor" begin + a = UnsafeRefArray{Int}(undef, (5,)) + @test length(a) == 5 + end + @testset "similar" begin + a = UnsafeRefArray{Int}(undef, 3) + b = similar(a, Float64, (4,)) + @test b isa UnsafeRefArray{Float64} + @test length(b) == 4 + end + @testset "collect_as identity" begin + a = UnsafeRefArray{Int}(undef, 3) + a[1] = 1; a[2] = 2; a[3] = 3 + b = collect_as(UnsafeRefArray{Int}, a) + @test b isa UnsafeRefArray{Int} + @test b !== a + @test b[1] == 1 && b[2] == 2 && b[3] == 3 + c = collect_as(UnsafeRefArray, a) + @test c isa UnsafeRefArray{Int} + @test c !== a + @test c[1] == 1 && c[2] == 2 && c[3] == 3 + end + end + @testset "`undef` construction without element type" begin @test_throws MethodError FixedSizeVector(undef, 1) @test_throws MethodError FixedSizeMatrix(undef, 1, 1) @@ -223,7 +251,7 @@ end end @testset verbose=true "test sets for multiple supported storage types" begin - for storage_type ∈ (((@isdefined Memory) ? (Memory,) : ())..., Vector) + for storage_type ∈ (((@isdefined Memory) ? (UnsafeRefArray,) : ())..., Vector) FSV = fsv(storage_type) FSM = fsm(storage_type) FSA = fsa(storage_type) @@ -549,7 +577,7 @@ end (FSV, FSV), ( if @isdefined Memory - ((FSV, Memory), (Memory, FSV)) + ((FSV, UnsafeRefArray), (UnsafeRefArray, FSV)) else () end