MemoryViews in interfaces
The intended purpose of the MemoryView type is to ease manipulation of memory-backed objects through a kind of low-level abstraction. Strings, substrings, Memory
, dense views of Matrix
and countless other types all have the same data representation, namely as simply a chunk of memory. This means they share important properties: Searching for a one-byte Char
inside a String
needs to ccall the exact same memchr
as searching for Int8
in a subarray of Memory
. Likewise, checking that two substrings are equal can use the same implementation as code checking that two bytearrays are equal. Obviously, writing the same implementation for each of these types is wasteful.
Unfortunately, Julia's system of abstract types are poorly equipped to handle this. This is because abstract types represent shared behaviour, whereas in this case, what unites these many different types are the underlying representation - exactly the thing that abstract types want to paper over!
MemoryViews.jl addresses this by introducing two types: At the bottom of abstraction, the simple MemoryView
type is most basic, unified instantiation of the underlying representation (a chunk of memory). At the top, the MemoryKind
trait controls dispatch such that the low-level MemoryView
implementation is called for the right types. The idea is that whenever you write a method that operates on "just" a chunk of memory, you implement it for MemoryView
. Then, you write methods with MemoryKind
to make sure all the proper function calls gets dispatched to your MemoryView
implementation.
Even if you only ever intend a method to work for, say, Vector
, it can still be a good idea to implement it for MemoryView
. First, it makes it explicit that you only use Vector
for its properties as a chunk of memory, and not for, say, its ability to be resized. Second, you can implement the method for ImmutableMemoryView
, letting both caller and callee know that the argument is not being mutated. Third, after implementing your method for MemoryView
, it may be easy to also make your method work for Memory
and other memory-backed types!
The MemoryKind
trait
MemoryKind
answers the question: Can instances of a type be treated as equal to its own memory view? For a type T
, MemoryKind(T)
returns one of two types:
NotMemory()
ifT
s are not equivalent to its own memory. Examples includeInt
, which has no memory representation because they are not heap allocated, andString
, which are backed by memory, but which are semantically different from anAbstractVector
containing its bytes.IsMemory{M}()
whereM
is a concrete subtype ofMemoryView
, if instances ofT
are equivalent to their own memory. Examples includeArray
s andCodeunits{String}
. For these objects, it's the case thatx == MemoryView(x)
.
julia> MemoryKind(Vector{Union{Int32, UInt32}})
IsMemory{MutableMemoryView{Union{Int32, UInt32}}}()
julia> MemoryKind(Matrix{String})
IsMemory{MutableMemoryView{String}}()
julia> MemoryKind(SubString{String})
NotMemory()
Implementing MemoryView
interfaces
When implementing a method that has a fast-past for memory-like types, you typically want to
- At the top level, dispatch on
MemoryKind
of your argument to funnel the memory-like objects into your optimisedMemoryView
function - At the low level, use
MemoryView
for the implementation of the optimised version
An example could be:
# Dispatch on `MemoryKind`
my_hash(x) = my_hash(MemoryKind(typeof(x)), x)
# For objects that are bytes, call the function taking only the memory
# representation of `x`
my_hash(::IsMemory{<:MemoryView{UInt8}}, x) = my_hash(ImmutableMemoryView(x))
# IsMemory with eltype other than UInt8 can't use the fast low-level function
my_hash(T::IsMemory, x) = my_hash(NotMemory(), x)
function my_hash(::NotMemory, x)
# fallback implementation
end
function my_hash(mem::ImmutableMemoryView{UInt8})
# some optimised low-level memory manipulation with `mem` of bytes
end
# Handle e.g. strings separately, since they are not semantically equal to
# an array of elements in memory, but for this method in particular,
# we want to treat strings as if they are.
function my_hash(x::Union{String, SubString{String}})
my_hash(MemoryView(x))
end