Function Composition vs Piping: Is there a performance difference?

I noticed cases where the pipe operator (e.g. |> ) is used over function composition (e.g. f(g(x))). I see composition used throughout the base code; so I assume that it’s the preferred convention.

Are there any underlying performance or algorithmic differences between the two? Or, is it only syntactic sugar?

This is just syntax. It shouldn’t make any difference in generated code.

1 Like

I think I’ve read that it’s not just syntax and affects stack traces etc. Might be wrong though

julia> test1(x) = exp(cos(log(x)))
test1 (generic function with 1 method)

julia> test2(x) = x |> log |> cos |> exp
test2 (generic function with 1 method)

julia> @code_typed test1(2.)
CodeInfo(
1 ─ %1 = invoke Main.log(_2::Float64)::Float64
│   %2 = invoke Main.cos(%1::Float64)::Float64
│   %3 = invoke Main.exp(%2::Float64)::Float64
└──      return %3
) => Float64

julia> @code_typed test2(2.)
CodeInfo(
1 ─ %1 = Main.log::Core.Compiler.Const(log, false)
│   %2 = invoke %1(_2::Float64)::Float64
│   %3 = Main.cos::Core.Compiler.Const(cos, false)
│   %4 = invoke %3(%2::Float64)::Float64
│   %5 = Main.exp::Core.Compiler.Const(exp, false)
│   %6 = invoke %5(%4::Float64)::Float64
└──      return %6
) => Float64

Piping does seem to make a longer stack trace, but if you make the substitution it looks exactly the same.

The LLVM looks about the same

julia> @code_llvm test1(2.)

;  @ REPL[1]:1 within `test1'
define double @julia_test1_264(double) {
top:
  %1 = call double @j_log_265(double %0)
  %2 = call double @j_cos_266(double %1)
  %3 = call double @j_exp_267(double %2)
  ret double %3
}

julia> @code_llvm test2(2.)

;  @ REPL[2]:1 within `test2'
define double @julia_test2_268(double) {
top:
; ┌ @ operators.jl:834 within `|>'
   %1 = call double @j_log_269(double %0)
   %2 = call double @j_cos_270(double %1)
   %3 = call double @j_exp_271(double %2)
; └
  ret double %3
}
7 Likes

Thanks!