Skip to content

Commit

Permalink
First version of the package
Browse files Browse the repository at this point in the history
  • Loading branch information
giordano committed Feb 19, 2024
1 parent 96614a9 commit 4d4be3c
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 0 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/UnitTests.yml
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name = "FixedSizeArrays"
uuid = "3821ddf9-e5b5-40d5-8e25-6813ab96b5e2"
authors = ["Mosè Giordano <[email protected]>"]
version = "0.1.0"

[compat]
Test = "1.11"
julia = "1.11"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"


[targets]
test = ["Test"]
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions src/FixedSizeArrays.jl
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 4d4be3c

Please sign in to comment.