New functions try_strides, is_ptr_loadable, and is_ptr_storable#60964
New functions try_strides, is_ptr_loadable, and is_ptr_storable#60964nhz2 wants to merge 11 commits into
try_strides, is_ptr_loadable, and is_ptr_storable#60964Conversation
|
I've moved the test reorganization to #61250 |
|
Question: is it a breaking change to replace |
|
JuliaLang/LinearAlgebra.jl#1619 is the companion PR in LinearAlgebra.jl |
| """ | ||
| can_ptr_load(A::AbstractArray)::Bool | ||
|
|
||
| Return `true` if a pointer to an `isbits` element in `A` can be used to load that element. Otherwise return `false`. |
There was a problem hiding this comment.
little confused how this interacts with
a pointer to any element of the array can be obtained
so we might have an array where we can obtain a pointer to an element, but we can neither load nor store from that pointer?
There was a problem hiding this comment.
Yes, for example, due to padding alignment it is possible to have a write only reinterpret array. If this were wrapped around a readonly array the result would be a both unwriteable and unreadable array.
There was a problem hiding this comment.
This violates strict aliasing so is not legal
There was a problem hiding this comment.
By "wrapped" I don't mean unsafe_wrap I mean using a new wrapper array type, this avoids any TBAA issues.
Here is a more explicit example.
# A read only vector wrapper type
struct ReadOnlyWrapper{T, A<:AbstractVector{T}} <: AbstractVector{T}
parent::A
end
Base.parent(A::ReadOnlyWrapper) = A.parent
Base.size(A::ReadOnlyWrapper) = size(A.parent)
Base.axes(A::ReadOnlyWrapper) = axes(A.parent)
Base.getindex(A::ReadOnlyWrapper, i::Int) = A.parent[i]
Base.cconvert(::Type{Ptr{T}}, A::ReadOnlyWrapper{T}) where {T} = Base.cconvert(Ptr{T}, A.parent)
Base.strides(A::ReadOnlyWrapper) = strides(A.parent)
Base.elsize(::Type{ReadOnlyWrapper{T,A}}) where {T,A} = Base.elsize(A)
Base.try_strides(A::ReadOnlyWrapper) = try_strides(A.parent)
Base.is_ptr_loadable(A::ReadOnlyWrapper) = is_ptr_loadable(A.parent)
# An array with padding
a = [(0x01, 0x0001)]
@show is_ptr_loadable(a)
@show is_ptr_storable(a)
b = reinterpret(UInt8, a);
@show is_ptr_loadable(b)
@show is_ptr_storable(b)
c = ReadOnlyWrapper(b)
@show is_ptr_loadable(c)
@show is_ptr_storable(c)This results in:
is_ptr_loadable(a) = true
is_ptr_storable(a) = true
is_ptr_loadable(b) = false
is_ptr_storable(b) = true
is_ptr_loadable(c) = false
is_ptr_storable(c) = falseThe ordering of the wrapping can be changed:
# An array with padding
a = [(0x01, 0x0001)]
@show is_ptr_loadable(a)
@show is_ptr_storable(a)
b = ReadOnlyWrapper(a)
@show is_ptr_loadable(b)
@show is_ptr_storable(b)
c = reinterpret(UInt8, b);
@show is_ptr_loadable(c)
@show is_ptr_storable(c)Results in:
is_ptr_loadable(a) = true
is_ptr_storable(a) = true
is_ptr_loadable(b) = true
is_ptr_storable(b) = false
is_ptr_loadable(c) = false
is_ptr_storable(c) = falseThere was a problem hiding this comment.
maybe the example was just intended for the flavor and not as a full motivator, but even here I think some cracks are showing. namely that the is_ptr_*able definitions here are making assumptions about the padding / layout of Tuple{UInt8, UInt16}. but if Julia were to, say, choose to start representing that type without padding then the traits would become wrong.
there's a reason that array_subpadding in reinterpretarray.jl is doing a bunch of nontrivial work, because to query this correctly I think we have to be careful about reading the runtime values of datatype_alignment and sizeof
There was a problem hiding this comment.
The is_ptr_*able methods match the checks in getindex and setindex!, and this is tested. Please compare is_ptr_*able methods to getindex and setindex! methods for ReinterpretArray.
julia/base/reinterpretarray.jl
Lines 416 to 420 in c07cf8f
This calls check_readable(a) which is:
julia/base/reinterpretarray.jl
Lines 226 to 231 in c07cf8f
Julia cannot arbitrarily change the padding of isbits because they are required to be C compatible.
There was a problem hiding this comment.
do they match the checks though? because check_readable and check_writable call array_subpadding which is doing a bunch of work to read the type layout. or put another way:
function is_ptr_loadable(a::ReinterpretArray{T,N,S} where N) where {T,S}
is_ptr_loadable(parent(a)) && (a.readable || array_subpadding(T, S))
end
function is_ptr_storable(a::ReinterpretArray{T,N,S} where N) where {T,S}
is_ptr_storable(parent(a)) && (a.writable || array_subpadding(S, T))
endwhy isn't is_ptr_*able(a) = is_ptr_*able(parent(a)) sufficient?
There was a problem hiding this comment.
Only checking the parent isn't enough to ensure there are no padding issues caused by the reinterpret. For that reason (a.readable || array_subpadding(T, S)) is also checked.
The tests in test/reinterpretarray.jl lines 392 and 395 fail with is_ptr_*able(a) = is_ptr_*able(parent(a)).
There was a problem hiding this comment.
The practical point is that I want zip_crc32(reinterpret_array_with_strange_padding_issues) to throw some error instead of silently returning something undefined.
| function try_substrides(strds::Tuple{Int, Vararg{Int}}, I::Tuple{AbstractRange, Vararg{Any}}) | ||
| rest = try_substrides(tail(strds), tail(I)) | ||
| isnothing(rest) && return nothing | ||
| (first(strds)*Int(step(first(I))), rest...) |
There was a problem hiding this comment.
Yes, try_strides should return Union{Dims, Nothing}, but step(first(I)) might not be an Int, for example:
julia> a = view([1,2,3], Int128(1):Int128(2):Int128(3))
2-element view(::Vector{Int64}, 1:2:3) with eltype Int64:
1
3
julia> step(first(a.indices)) |> typeof
Int128| true | ||
| end | ||
|
|
||
| function try_strides(a::ReinterpretArray{T,<:Any,S,<:AbstractArray{S},IsReshaped}) where {T,S,IsReshaped} |
There was a problem hiding this comment.
maybe want _checkcontiguous fast path like strides has? same for ReshapedArray
There was a problem hiding this comment.
Yes, this could be done as a future optimization.
| !!! compat "Julia 1.14" | ||
| This function requires at least Julia 1.14. | ||
| """ | ||
| try_strides(A::AbstractArray) = nothing |
There was a problem hiding this comment.
| try_strides(A::AbstractArray) = nothing | |
| function try_strides(A::AbstractArray) | |
| try | |
| strides(A) | |
| catch e | |
| (e isa MethodError) ? return nothing : rethrow() | |
| end | |
| end |
should we "try" harder ? also this means the invariant you claim in the docstring
Return a tuple of the memory strides in each dimension if `A` is strided.
Otherwise return `nothing`.
will be more true. otherwise, any existing strided array type that doesn't yet define try_strides will violate this.
There was a problem hiding this comment.
I think it is better to go through and add these methods to existing strided array types. This is good chance to check they are actually following this new strided array interface. For example, JuliaLang/LinearAlgebra.jl#1618 #53996
Co-authored-by: Andy Dienes <51664769+adienes@users.noreply.github.com>
try_strides, can_ptr_load, and can_ptr_storetry_strides, is_ptr_loadable, and is_ptr_storeable
try_strides, is_ptr_loadable, and is_ptr_storeabletry_strides, is_ptr_loadable, and is_ptr_storable
|
JuliaIO/InputBuffers.jl#16 is an example use case |
|
JuliaPy/PythonCall.jl#777 using ZipArchives, FixedSizeArrays, PythonCall, BenchmarkTools, Random
data = rand(Xoshiro(1234), UInt8, 300000000);
f_data = FixedSizeArray(copy(data));
py_data = PyArray(copy(data));
@btime zip_crc32($data)
@btime zip_crc32($f_data)
@btime zip_crc32($py_data)Julia 1.12.6: With |
|
Triage thinks this should behave more like a trait. |
What would that look like? |
|
(relaying what I remember from the discussion, not my own thoughts) the argument in was that this function is being proposed for use to switch to strided algorithm. but this is what traits do, and the so this should be closer to something that looks like furthermore, (back to my personal thought) my interpretation of the discussion was that the desired incarnation of this feature would look more like something that can close the referenced issues, not just update them. |
|
So basically implementing something like #10889 (comment) abstract MemLayout
abstract Strided <: MemLayout
abstract UnitStrided <: Strided
abstract Contiguous <: UnitStridedAnd then a function mapping from That sounds useful but still not complete. If the memory layout cannot be computed based on the type, I want to be able to get the strides if they exist at runtime. Also once the memory layout is known I want to know if loads and stores to a pointer to an element are equivalent to using Maybe these requirements are not needed by |
|
something like that, although I'm not sure what the difference between
at the very least, the type should be able to give you enough information to know if a it might be useful (at least for me, who remains a bit confused) to come up with a table of types/values that would occupy different spots in that proposed type hierarchy or different results from if |
|
Yes using pointers is not recommended, but they are needed when calling C and Julia functions that operate on pointers. I'm also not sure what I'm assuming Here are some examples of arrays that do not have transpose(zeros(1,5))
view(zeros(5), 2:1:4)I think these types of arrays are easy to run into. Here are examples of arrays that do not have reshape(view(zeros(4,4), 1:4, 1:2), 8)
reinterpret(UInt16, view(zeros(UInt8, 9, 8), 1:8, 1:2:8))I'm not sure if these exist in the wild, but the existing
In general |
|
Looking at @stevengj 's function in Blosc.jl |
This is an alternative to #60894 and #61807
It adds three new functions to the strided array interface.
try_strides,is_ptr_loadable, andis_ptr_storable.try_stridesis similar tostridesexcept it returnsnothingwhen the array isn't strided.is_ptr_loadableandis_ptr_storablecan be used to check if a pointer to an array element can be used to get or set the value.XRef: #54715 #59435 #10889