1

Is it possible to use in Julia something like pointers or references as in C/C++ or C#? I'm wondering because it will be helpful to pass weighty objects as a pointer/reference but not as a value. For example, using pointers memory to store an object can be allocated once for the whole program and then the pointer can be passed through the program. As I can imagine, it will boost performance in memory and computing power usage.

Simple C++ code showing what I'm trying to execute in Julia:

#include <iostream> 

void some_function(int* variable){       // declare function 
    *variable += 1;                      // add a value to the variable
}

int main(){                                
    int very_big_object = 1;             // define variable
    some_function( &very_big_object );   // pass pointer of very_big_object to some_function
    std::cout << very_big_object;        // write value of very_big_object to stdout

    return 0;                            // end of the program
}

Output:

2

New object is created, its pointer is then passed to some_funciton that modifies this object using passed pointer. Returning of the new value is not necessary, because program edited original object, not copy. After executing some_function value of the variable is print to see how it has changed.

2 Answers2

3

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.
cbk
  • 4,225
  • 6
  • 27
3

While Julia uses "pass-by-sharing" and normally you do not have to/do not want to use pointers in some cases you actually want do it and you can!

You construct pointers by Ref{Type} and then deference them using []. Consider the following function mutating its argument.

function mutate(v::Ref{Int})
       v[] = 999
end

This can be used in the following way:

julia> vv = Ref(33)
Base.RefValue{Int64}(33)

julia> mutate(vv);

julia> vv
Base.RefValue{Int64}(999)

julia> vv[]
999

For a longer discussion on passing reference in Julia please have a look at this post How to pass an object by reference and value in Julia?

Przemyslaw Szufel
  • 40,002
  • 3
  • 32
  • 62