Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/src/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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
```
Expand All @@ -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:
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions ext/FixedSizeArraysRandomExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 9 additions & 3 deletions src/FixedSizeArray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -418,22 +424,22 @@ 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}
"""
FixedSizeVectorDefault{T}(undef, size::NTuple{1,Int})
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}
"""
FixedSizeMatrixDefault{T}(undef, size::NTuple{2,Int})
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}
10 changes: 7 additions & 3 deletions src/FixedSizeArrays.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
102 changes: 102 additions & 0 deletions src/refarray.jl
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +5 to +7
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we can't use MemoryRef directly?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because MemoryRef <: Ref, whereas FixedSizeArray must be backed by a DenseVector:

struct FixedSizeArray{T,N,Mem<:DenseVector{T}} <: DenseArray{T,N}
    mem::Mem
    size::NTuple{N,Int}
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)[])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which part are you trying to @inbounds?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both making the ref, and dereferencing it, both of which checks bounds.

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
34 changes: 31 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -549,7 +577,7 @@ end
(FSV, FSV),
(
if @isdefined Memory
((FSV, Memory), (Memory, FSV))
((FSV, UnsafeRefArray), (UnsafeRefArray, FSV))
else
()
end
Expand Down
Loading