While you can manually obtain pointers for Julia objects, this is actually not necessary to obtain the performance and results you want. As the manual notes:
Julia function arguments follow a convention sometimes called "pass-by-sharing", which means that values are not copied when they are passed to functions. Function arguments themselves act as new variable bindings (new locations that can refer to values), but the values they refer to are identical to the passed values. Modifications to mutable values (such as Arrays) made within a function will be visible to the caller. This is the same behavior found in Scheme, most Lisps, Python, Ruby and Perl, among other dynamic languages.
consequently, you can just pass the object to the function normally, and then operate on it in-place. Functions that mutate their input arguments (thus including any in-place functions) by convention in Julia have names that end in !
. The only catch is that you have to be sure not to do anything within the body of the function that would trigger the object to be copied.
So, for example
function foo(A)
B = A .+ 1 # This makes a copy, uh oh
return B
end
function foo!(A)
A .= A .+ 1 # Mutate A in-place
return A # Technically don't have to return anything at all, but there is also no performance cost to returning the mutated A
end
(note in particular the .
in front of .=
in the second version; this is critical. If we had left that off, we actually would not have mutated A
, but just reassigned the name A
(within the scope of the function) to refer to the result of the RHS, so this actually would be entirely equivalent to the first version if it weren't for that .
)
If we then benchmark these, you can see the difference clearly:
julia> large_array = rand(10000,1000);
julia> using BenchmarkTools
julia> @benchmark foo(large_array)
BechmarkTools.Trial: 144 samples with 1 evaluations.
Range (min … max): 23.315 ms … 78.293 ms ┊ GC (min … max): 0.00% … 43.25%
Time (median): 27.278 ms ┊ GC (median): 0.00%
Time (mean ± σ): 34.705 ms ± 12.316 ms ┊ GC (mean ± σ): 23.72% ± 23.04%
▁▄▃▄█ ▁
▇█████▇█▇▆▅▃▁▃▁▃▁▁▁▄▁▁▁▁▃▁▁▃▁▁▁▁▁▁▁▁▁▁▁▁▁▅▃▄▁▄▄▃▄▆▅▆▃▅▆▆▆▅▃ ▃
23.3 ms Histogram: frequency by time 55.3 ms <
Memory estimate: 76.29 MiB, allocs estimate: 2.
julia> @benchmark foo!(large_array)
BechmarkTools.Trial: 729 samples with 1 evaluations.
Range (min … max): 5.209 ms … 9.655 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 6.529 ms ┊ GC (median): 0.00%
Time (mean ± σ): 6.845 ms ± 955.282 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
▁ ▄▇██▇▆ ▁ ▁
▂▂▃▂▄▃▇▄▅▅████████▇█▆██▄▅▅▆▆▄▅▆▇▅▆▄▃▃▄▃▄▃▃▃▂▂▃▃▃▃▃▃▂▃▃▃▂▂▂▃ ▃
5.21 ms Histogram: frequency by time 9.33 ms <
Memory estimate: 0 bytes, allocs estimate: 0.
Note especially the difference in memory usage and allocations in addition to the ~4x time difference. At this point, pretty much all the time is being spent on the actual addition, so the only thing left to optimize if this were performance-critical code would be to make sure that the code is efficiently using all of your CPU's SIMD vector registers and instructions, which you can do with the LoopVectorization.jl package:
using LoopVectorization
function foo_simd!(A)
@turbo A .= A .+ 1 # Mutate A in-place. Equivalentely @turbo @. A = A + 1
return A
end
julia> @benchmark foo_simd!(large_array)
BechmarkTools.Trial: 986 samples with 1 evaluations.
Range (min … max): 4.873 ms … 7.387 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 4.922 ms ┊ GC (median): 0.00%
Time (mean ± σ): 5.061 ms ± 330.307 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
█▇▄▁▁ ▂▂▅▁
█████████████▆▆▇▆▅▅▄▅▅▅▆▅▅▅▅▅▅▆▅▅▄▁▄▅▄▅▄▁▄▁▄▁▅▄▅▁▁▄▅▁▄▁▁▄▁▄ █
4.87 ms Histogram: log(frequency) by time 6.7 ms <
Memory estimate: 0 bytes, allocs estimate: 0.
This buys us a bit more performance, but it looks like for this particular case Julia's normal compiler probably already found some of these SIMD optimizations.
Now, if for any reason you still want a literal pointer, you can always get this with Base.pointer
, though note that this comes with some significant caveats and is generally not what you want.
help?> Base.pointer
pointer(array [, index])
Get the native address of an array or string, optionally at a given location index.
This function is "unsafe". Be careful to ensure that a Julia reference to array exists
as long as this pointer will be used. The GC.@preserve macro should be used to protect
the array argument from garbage collection within a given block of code.
Calling Ref(array[, index]) is generally preferable to this function as it guarantees
validity.