Base.getindex(node::Element, key::AbstractString) = begin
next = node.value
while !isnothing(next)
if _equals(getname(next), key) return next end
next = getnext(next)
end
return error("no element with key: $key")
end
When I’m trying to access field name of the struct Element in function getindex using variable next there is 1 allocation (accessing any field except name does not cause allocations), but when I do it anywhere else it does not cause allocation. I don’t understand why accessing this field causes additional memory usage and how to fix this issue?
@code_warn for this function provides the following results:
As Kristoffer said, it’s basically impossible to help here without more code. I was able to modify your provided code in order to define the struct and I observe no allocations in accessing the name field:
julia> abstract type AbstractElement end
julia> mutable struct Element{T<:AbstractElement} <: AbstractElement
input::String
name::UnitRange{Int64}
attributes::Union{Nothing, Int}
value::Union{T, Nothing}
parent::Union{Element, Nothing}
next::Union{Element, Nothing}
end
julia> let e = Element{Element}("hi", 1:10, nothing, nothing, nothing, nothing)
@btime $e.name
end
1.299 ns (0 allocations: 0 bytes)
1:10
I suspect the allocations are caused by something else. What could be causing those allocations though is anybody’s guess, since nobody but you knows how you defined getname, getnext, _equals, or Attribute.
using Test
using BenchmarkTools
abstract type AbstractElement end
mutable struct Element{T<:AbstractElement} <: AbstractElement
input::String
name::UnitRange{Int64}
attributes::Union{Nothing, Int}
value::Union{T, Nothing}
parent::Union{Element, Nothing}
next::Union{Element, Nothing}
end
example(node::Element) = begin
while !isnothing(node)
if node.name == 1:1 return node end
node = node.next
end
return error("error")
end
@testset "example" begin
first = Element{Element}("", 2:2, nothing, nothing, nothing, nothing)
second = Element{Element}("", 1:1, nothing, nothing, nothing, nothing)
first.next = second
@code_warntype example(first)
@btime example($first)
end
You can just copypaste this snippet, it works and shows the problem
Ah, I see the problem now. This eluded me for a bit. The problem is that you’re using
::Union{Element, Nothing}
for the field next. This looks like a small union, but it’s actually not because Element is really Element{T} where {T}, a UnionAll!
Here’s a modified version of your code that doesn’t allocate and is two orders of magnitude faster:
abstract type AbstractElement end
mutable struct Element{T<:AbstractElement} <: AbstractElement
input::String
name::UnitRange{Int64}
attributes::Union{Nothing, Int}
value::Union{T, Nothing}
parent::Union{Element{T}, Nothing}
next::Union{Element{T}, Nothing}
end
example(node::Element) = begin
while !isnothing(node)
if node.name == 1:1 return node end
node = node.next
end
return error("error")
end
first = Element{Element}("", 2:2, nothing, nothing, nothing, nothing)
second = Element{Element}("", 1:1, nothing, nothing, nothing, nothing)
first.next = second
@btime example($first)
So I actually didn’t need the extra information I requested earlier and if I was more perceptive, I could have noticed it right away. But having the code at least let me fiddle around until I stumbled upon the root problem
Yes, there can be lots of little performance gotchas like this when you get into type unstable code in Julia. I wish you the best of luck in the transition, I think you’ll find it’s worth your time!
Actually, I would like to note 2 points: First, Julia allows write really high-performance code. Second, designing the code in Julia is a bit harder than in Java or Python due to absence of builtin interfaces. But, in general, it is very interesting language
I have a very similar issue that I think fits with what the OP is saying, so I will post here instead of starting a new thread (obviously feel free to tell me that I should move this to a new thread).
The problem has to do with seeing more allocations when accessing a function that is inside a struct than when using the same function directly. Please consider the following MWE. The function in question is tanh. In function first(), tanh is called directly. In function second() it is called from within struct dummy. Function third() is calls tanh from a struct where no type has been provided.
I hope the following code makes it clearer:
struct Dummy
f::Function
end
struct DummyNoType
f
end
function runme()
a = randn(1000)
dummy = Dummy(tanh)
dummynotype = DummyNoType(tanh)
v = 0.0
function first()
for i in 1:1000
v += tanh(a[i])
end
end
function second()
for i in 1:1000
v += dummy.f(a[i])
end
end
function third()
for i in 1:1000
v += dummynotype.f(a[i])
end
end
@time first()
@time second()
@time third()
end
If execute run() I get:
0.000057 seconds (2.00 k allocations: 31.250 KiB) # called first()
0.000081 seconds (3.00 k allocations: 46.875 KiB) # called second()
0.000089 seconds (3.00 k allocations: 46.875 KiB) # called third()
Why does the call to second() result in more memory allocations? Thanks for reading this.