Separating values from Iterator.product() tuple

I am calling a function many times using pmap(), where some of the arguments are passed each time while others are iterated over. To produce the correct permutations of the arguments I am utilising the Iterator library. A MWE looks like:

julia> using Distributed

julia> function test(args)
           @show args[1], args[2]
       end
test (generic function with 1 method)

julia> pmap(test, Iterators.product(Iterators.repeated("Hello", 1), "world"));
(args[1], args[2]) = ("Hello", 'w')
(args[1], args[2]) = ("Hello", 'o')
(args[1], args[2]) = ("Hello", 'r')
(args[1], args[2]) = ("Hello", 'l')
(args[1], args[2]) = ("Hello", 'd')

In my actual code I have quite a few arguments and I would like for them to have comprehensible names within the function. However, Iterator.product() produces a tuple which is then passed, thus my variables are referenced within the function by args[i] - I consider this to be undesirable. The most obvious way to resolve this is to assign each element of the tuple to a new variable within the function, i.e.

julia> function test(args)
           arg1 = args[1]
           arg2 = args[2]
           @show arg1, args2
       end

This results in additional allocations which, over several million iterations, is also not desirable. Is there a way to either separate the values out of the tuple so that they are passed on their own or somehow refer to the elements of the tuple by other names, i.e. aliases for variables?

EDIT: There are no additional allocations; see post #3 for benchmarks highlighting such.

Are you looking for:

function test((arg1,arg2))
    @show arg1, arg2
end

But your example shouldn’t really have any overhead, since this kind of tuple-destructing should be statically inferred by the compiler and extra allocations should be eliminated if you’re not accessing args elsewhere. Are you sure you’re not measuring allocations from elsewhere?

2 Likes

@simeonschaub That certainly looks like what I am after, thanks!

If I am honest, I hadn’t benchmarked the functions (my bad :flushed:), but it is certainly ugly! For those interested, the @btime benchmarks are as follows:

function f1(args)
    @show args[1], args[2]
end

> 107.107 μs (197 allocations: 6.83 KiB)

function f2(args)
    arg1 = args[1]
    arg2 = args[2]
    @show arg1, arg2
end

> 109.752 μs (197 allocations: 6.83 KiB)

function f3((arg1, arg2))
    @show arg1, arg2
end

> 111.287 μs (197 allocations: 6.83 KiB)

What exactly are you benchmarking? You’re probably just measuring the time it takes to print the args and the overhead due to pmap. Still, you can see that they all take the same amount of time within the expected deviation. I really wouldn’t bother with these micro-optimizations, performance bottlenecks are very likely elsewhere.

I was more illustrating your point that there are no additional allocations depending on the form of the function - this was not intuitive to me. Out of interest then, would you mind explaining why adding the lines arg = args[i] does not increase allocation number? It seems to me that it should.

If you look at the type of tuples, you can see, that it encodes the type of each of its elements:

julia> typeof((1,2,3.0))
Tuple{Int64,Int64,Float64}

You can then basically imagine a Tuple as just a bunch of independent variables of known types, grouped together. If you’re indexing a tuple with an integer literal like 1 (or if the index can be statically inferred), the compiler can just replace this with the corresponding value. Since this happens at compile time, there are no type instabilities, and if the compiler sees that the tuple is never used, it doesn’t have to allocate it and can just pass the values. (If the tuple doesn’t contain any mutable objects, this can be done completely on the stack)

1 Like

Assigning something to a variable in Julia just means attaching a name to that value in your code. Unlike some other languages, the syntax a = b does not create a copy or call some sort of assignment constructor in Julia.

It has no effect on the memory allocation of the resulting compiled code because the value itself is completely unaffected.

4 Likes