Broadcasting of minus as prefix operator

Hello, I have the following technical question: when using - as prefix operator, does it get broadcasted as it would be an operator with a dot, i.e. is this

x = 2
y = [1,2,3]
z = -x .+ y

equivalent to

x = 2
y = [1,2,3]
z = -1 .* x .+ y

because it would matter in terms of performance.

At least the @code_lowered after defining a function is not the same:

f(x,y) = -x .+ y
CodeInfo(
91 1 ─ %1 = Base.Broadcast.materialize                               │
   │   %2 = Base.Broadcast.broadcasted                               │
   │   %3 = -x                                                       │
   │   %4 = (%2)(Main.:+, %3, y)                                     │
   │   %5 = (%1)(%4)                                                 │
   └──      return %5                                                │
)
f(x,y) = -1 .* x .+ y
CodeInfo(
91 1 ─ %1 = Base.Broadcast.materialize                               │
   │   %2 = Base.Broadcast.broadcasted                               │
   │   %3 = Base.Broadcast.broadcasted                               │
   │   %4 = (%3)(Main.:*, -1, x)                                     │
   │   %5 = (%2)(Main.:+, %4, y)                                     │
   │   %6 = (%1)(%5)                                                 │
   └──      return %6    

Thanks.

The Unary - operator has higher precedence than the addition operator, so yes.

Performance wise, they’re virtually equivalent:

julia> using BenchmarkTools

julia> f(x,y) = -x .+ y
f (generic function with 1 method)

julia> g(x,y) = -1 .* x .+ y
g (generic function with 1 method)

julia> @btime f($x, $y)
  32.161 ns (1 allocation: 112 bytes)
3-element Array{Int64,1}:
 -1
  0
  1

julia> @btime g($x, $y)
  32.193 ns (1 allocation: 112 bytes)
3-element Array{Int64,1}:
 -1
  0
  1
1 Like

It is equivalent to z = (-x) .+ y. At least that’s what I take from @code_lowered:

julia> f() = z = -x .+ y
f (generic function with 1 method)

julia> @code_lowered f()
CodeInfo(
1 1 ─ %1 = Base.Broadcast.materialize                                                                                                                                                                                              │
  │   %2 = Base.Broadcast.broadcasted                                                                                                                                                                                              │
  │   %3 = -Main.x                                                                                                                                                                                                                 │
  │   %4 = (%2)(Main.:+, %3, Main.y)                                                                                                                                                                                               │
  │   %5 = (%1)(%4)                                                                                                                                                                                                                │
  │        z = %5                                                                                                                                                                                                                  │
  └──      return %5                                                                                                                                                                                                               │
)

Sorry, I was a little bit misleading in my example, I wanted x to be an array as well. Now I have the test and it is not the same speed !!!

julia> using BenchmarkTools

julia> x = [4,5,6];

julia> y = [1,2,3];

julia> f(x,y) = -1 .* x .+ y ;

julia> g(x,y) = -x .+ y ;

julia> @code_lowered f(x,y)
CodeInfo(
1 1 ─ %1 = Base.Broadcast.materialize                                                               │
  │   %2 = Base.Broadcast.broadcasted                                                               │
  │   %3 = Base.Broadcast.broadcasted                                                               │
  │   %4 = (%3)(Main.:*, -1, x)                                                                     │
  │   %5 = (%2)(Main.:+, %4, y)                                                                     │
  │   %6 = (%1)(%5)                                                                                 │
  └──      return %6                                                                                │
)

julia> @code_lowered g(x,y)
CodeInfo(
1 1 ─ %1 = Base.Broadcast.materialize                                                               │
  │   %2 = Base.Broadcast.broadcasted                                                               │
  │   %3 = -x                                                                                       │
  │   %4 = (%2)(Main.:+, %3, y)                                                                     │
  │   %5 = (%1)(%4)                                                                                 │
  └──      return %5                                                                                │
)

julia> @btime f($x,$y)
  107.821 ns (1 allocation: 112 bytes)
3-element Array{Int64,1}:
 -3
 -3
 -3

julia> @btime g($x,$y)
  201.928 ns (2 allocations: 224 bytes)
3-element Array{Int64,1}:
 -3
 -3
 -3

This is bad, I would say. Because -x is much more convenient than -1 .* x when dealing with longer expressions.

If x is an array, just dot the - as well:

julia> f(x,y) = .-x .+ y
f (generic function with 1 method)

julia> g(x,y) = -1 .* x .+ y
g (generic function with 1 method)

julia> @btime f($x,$y)
  39.354 ns (1 allocation: 112 bytes)
3-element Array{Int64,1}:
 -3
 -3
 -3

julia> @btime g($x,$y)
  39.315 ns (1 allocation: 112 bytes)
3-element Array{Int64,1}:
 -3
 -3
 -3

The reason it’s gotten slower if you don’t dot it as well is because if an expression has all dots, the compiler can fuse all the broadcasts into a single call, greatly reducing overhead. The same can be achieved with @. which dots every operator/function call:

julia> h(x,y) = @. -x + y
h (generic function with 1 method)

julia> @btime h($x,$y)
  39.315 ns (1 allocation: 112 bytes)
3-element Array{Int64,1}:
 -3
 -3
 -3
2 Likes

Thanks, I accidentally tried -.x .+ y before writing this post and this did not worked, of cause.

Yes, I know, just did not realiesed that using .- is the right way for writing it, because usually for a function you use f.() so I thougt it should be -.x.

- is an operator though, and those usually go with the dot beforehand:

julia> a = [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> b = [3,4,5]
3-element Array{Int64,1}:
 3
 4
 5

julia> a .+ b
3-element Array{Int64,1}:
 4
 6
 8

The other way around is even in “function syntax” wrong:

julia> .-(a)
3-element Array{Int64,1}:
 -1
 -2
 -3

julia> -.(a)
ERROR: syntax: numeric constant "-." cannot be implicitly multiplied because it ends with "."

Yeah, will remember that in future :wink: