diff --git a/.github/workflows/UnitTests.yml b/.github/workflows/UnitTests.yml new file mode 100644 index 0000000..039fd92 --- /dev/null +++ b/.github/workflows/UnitTests.yml @@ -0,0 +1,46 @@ +name: Unit Tests + +on: + push: + branches: "main" + pull_request: + release: + +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: always. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Julia ${{ matrix.julia_version }} - ${{ matrix.os }} - ${{ matrix.julia_arch }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: + - macos-latest + - ubuntu-latest + - windows-latest + julia_version: + - "nightly" + julia_arch: + - x64 + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v1 + with: + arch: ${{ matrix.julia_arch }} + version: ${{ matrix.julia_version }} + - uses: julia-actions/cache@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: lcov.info + continue-on-error: true diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..bb3d20c --- /dev/null +++ b/Project.toml @@ -0,0 +1,15 @@ +name = "FixedSizeArrays" +uuid = "3821ddf9-e5b5-40d5-8e25-6813ab96b5e2" +authors = ["Mosè Giordano "] +version = "0.1.0" + +[compat] +Test = "1.11" +julia = "1.11" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + + +[targets] +test = ["Test"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..ecbb559 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +## `FixedSizeArrays.jl` + +`FixedSizeArrays.jl` is a proof-of-concept package for the [Julia programming language](https://julialang.org/) which implements mutable fixed-size arrays, which means the lenght of the array is constant and is amenable to be [constant-propagated](https://en.wikipedia.org/wiki/Constant_folding) at compile-time when possible. +This is an alternative implementation to [`MArray`](https://juliaarrays.github.io/StaticArrays.jl/stable/pages/api/#StaticArraysCore.MArray) from [`StaticArrays.jl`](https://github.com/JuliaArrays/StaticArrays.jl). + +Main differences between `FixedSizeArray` and `MArray` are: + +* `FixedSizeArray` is based on the `Memory` type introduced in Julia v1.11, `MArray` is backed by tuples; +* the size of the array is part of the type parameters of `MArray`, this isn't the case for `FixedSizeArray`, where the size is only a constant field of the data structure. + +Note: `FixedSizeArray`s are not guaranteed to be stack-allocated, in fact they will more like *not* be stack-allocated. +However, in some *extremely* simple cases the compiler may be able to completely elide their allocations: +```julia +julia> using FixedSizeArrays + +julia> @noinline f(A::AbstractArray) = length(A) +f (generic function with 1 method) + +julia> g() = f(FixedSizeVector{Float64}(undef, 3)) +g (generic function with 1 method) + +julia> h() = f(Vector{Float64}(undef, 3)) +h (generic function with 1 method) + +julia> code_llvm(g) +``` +```llvm +; Function Signature: g() +; @ REPL[3]:1 within `g` +define i64 @julia_g_511() #0 { +top: + ret i64 3 +} + +``` +```julia +julia> code_llvm(h) +``` +```llvm +; Function Signature: h() +; @ REPL[4]:1 within `h` +define i64 @julia_h_693() #0 { +top: + %gcframe1 = alloca [3 x ptr], align 16 + call void @llvm.memset.p0.i64(ptr align 16 %gcframe1, i8 0, i64 24, i1 true) + %pgcstack = call ptr inttoptr (i64 7452881148 to ptr)(i64 262) #10 + store i64 4, ptr %gcframe1, align 16 + %task.gcstack = load ptr, ptr %pgcstack, align 8 + %frame.prev = getelementptr inbounds ptr, ptr %gcframe1, i64 1 + store ptr %task.gcstack, ptr %frame.prev, align 8 + store ptr %gcframe1, ptr %pgcstack, align 8 +; ┌ @ boot.jl:576 within `Array` +; │┌ @ boot.jl:514 within `GenericMemory` + %"Memory{Float64}[]" = call ptr @jl_alloc_genericmemory(ptr nonnull @"+Core.GenericMemory#695.jit", i64 3) +; │└ +; │ @ boot.jl:577 within `Array` + %.data_ptr = getelementptr inbounds { i64, ptr }, ptr %"Memory{Float64}[]", i64 0, i32 1 + %0 = load ptr, ptr %.data_ptr, align 8 + %gc_slot_addr_0 = getelementptr inbounds ptr, ptr %gcframe1, i64 2 + store ptr %"Memory{Float64}[]", ptr %gc_slot_addr_0, align 16 + %ptls_field = getelementptr inbounds ptr, ptr %pgcstack, i64 2 + %ptls_load = load ptr, ptr %ptls_field, align 8 + %"new::Array" = call noalias nonnull align 8 dereferenceable(32) ptr @ijl_gc_pool_alloc_instrumented(ptr %ptls_load, i32 800, i32 32, i64 4645053728) #8 + %"new::Array.tag_addr" = getelementptr inbounds i64, ptr %"new::Array", i64 -1 + store atomic i64 4645053728, ptr %"new::Array.tag_addr" unordered, align 8 + %1 = getelementptr inbounds ptr, ptr %"new::Array", i64 1 + store ptr %0, ptr %"new::Array", align 8 + store ptr %"Memory{Float64}[]", ptr %1, align 8 + %"new::Array.size_ptr" = getelementptr inbounds i8, ptr %"new::Array", i64 16 + store i64 3, ptr %"new::Array.size_ptr", align 8 + store ptr %"new::Array", ptr %gc_slot_addr_0, align 16 +; └ + %2 = call i64 @j_f_699(ptr nonnull %"new::Array") + %frame.prev10 = load ptr, ptr %frame.prev, align 8 + store ptr %frame.prev10, ptr %pgcstack, align 8 + ret i64 %2 +} +``` + +> [!WARNING] +> This package should currently be used only to experiment with the idea of `Memory`-backed fixed-size arrays, it's highly non-optimised, absolutely don't use it for production. diff --git a/src/FixedSizeArrays.jl b/src/FixedSizeArrays.jl new file mode 100644 index 0000000..34f486b --- /dev/null +++ b/src/FixedSizeArrays.jl @@ -0,0 +1,45 @@ +module FixedSizeArrays + +export FixedSizeArray, FixedSizeVector, FixedSizeMatrix + +mutable struct FixedSizeArray{T,N} <: DenseArray{T,N} + ref::MemoryRef{T} + const size::NTuple{N,Int} +end + +const FixedSizeVector{T} = FixedSizeArray{T,1} +const FixedSizeMatrix{T} = FixedSizeArray{T,2} + +eval(:(function (self::Type{FixedSizeArray{T,N}})(::UndefInitializer, size::Vararg{Int,N}) where {T,N} + mem = fieldtype(fieldtype(self, :ref), :mem)(undef, prod(size)) + return $(Expr(:new, :self, :(Core.memoryref(mem)), :(size))) +end)) + +function Base.setindex!(A::FixedSizeArray{T}, x, i::Int) where {T} + Base.@_noub_if_noinbounds_meta + @boundscheck (i - 1)%UInt < length(A)%UInt || throw_boundserror(A, (i,)) + Core.memoryrefset!(Core.memoryref(A.ref, i, false), x isa T ? x : convert(T,x)::T, :not_atomic, false) + return A +end +function Base.setindex!(A::FixedSizeArray{T}, x, i1::Int, i2::Int, I::Int...) where {T} + @inline + Base.@_noub_if_noinbounds_meta + @boundscheck checkbounds(A, i1, i2, I...) # generally _to_linear_index requires bounds checking + Core.memoryrefset!(Core.memoryref(A.ref, Base._to_linear_index(A, i1, i2, I...), false), x isa T ? x : convert(T,x)::T, :not_atomic, false) + return A +end + +function Base.getindex(A::FixedSizeArray, i::Int) + Base.@_noub_if_noinbounds_meta + @boundscheck Base.ult_int(Base.bitcast(UInt, Base.sub_int(i, 1)), Base.bitcast(UInt, length(A))) || throw_boundserror(A, (i,)) + Core.memoryrefget(Core.memoryref(getfield(A, :ref), i, false), :not_atomic, false) +end +function Base.getindex(A::FixedSizeArray, i1::Int, i2::Int, I::Int...) + @inline + @boundscheck checkbounds(A, i1, i2, I...) # generally _to_linear_index requires bounds checking + return @inbounds A[Base._to_linear_index(A, i1, i2, I...)] +end + +Base.size(a::FixedSizeArray) = getfield(a, :size) + +end # module FixedSizeArrays diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..5413980 --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,14 @@ +using Test +using FixedSizeArrays + +@testset "FixedSizeArrays" begin + v = FixedSizeVector{Float64}(undef, 3) + @test length(v) == 3 + v .= 1:3 + @test v == 1:3 + + m = FixedSizeMatrix{Float64}(undef, 3, 3) + @test length(m) == 9 + m[:] .= 1:9 + @test m[:] == 1:9 +end