와 좋은 질문이네요. 덕분에 저도 원론적인 부분에서 다시 이해를 다잡는 좋은 기회가 되었습니다.
- 기본적으로 Julia의 Argument Passing 은 "pass-by-sharing"입니다. 이 점을 염두에 두셔야 합니다.
그럼 BenchmarkTools.jl
의 @btime
과 InteractiveUtils
의 @code_lowered
를 이용해서 각자의 코드가 내부적으로 어떻게 동작하는 지 알아봅시다.
-
test1
test1(C, A, B) 37.206 ns (1 allocation: 128 bytes) CodeInfo( 1 ─ C@_5 = C@_2 │ %2 = Base.broadcasted(Main.:+, A, B) │ C@_5 = Base.materialize(%2) └── return Main.nothing )
이는
A .+ B
를 계산하고 이를C
와 다른 변수에 대입했다는 것입니다. 코드를 설명하자면 broadcast(.+
) 할때broadcasted
를 통해 처리하지만 실제로는 계산이 매번 일어나지 않고 마지막에materialize
에서 한번에 일어납니다. (lazy evaluation)본론으로 들어가서
=
는 이름을 바꾼다고 생각하시면 됩니다. 위에서 언급했듯이 argument passing인 경우에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."
라고 되어있죠? new variable "binding"즉 function argument의 변수 이름이 원래 외부의 변수 이름과 다르더라도 같은 object를 가르키지만, 이름은 다르게 불리울 수 있다는 것을 뜻합니다. 이걸 이해하시고 엄청 오래된 글이지만 이 글을 보시면 이해가 잘되실 겁니다. 여기서 설명하긴 너무 길어서요 ㅜㅜ
-
test2
test2(C, A, B) 15.809 ns (0 allocations: 0 bytes) CodeInfo( 1 ─ %1 = Base.broadcasted(Main.:+, A, B) │ Base.materialize!(C, %1) └── return Main.nothing )
이는
A .+ B
를 계산하고 이를C
에 대입했다는 것입니다. .= 은 실제로 내용을 바꾸는 겁니다.materialize!
라고 되어있죠.!
는 argument를 바꾼다고 함수에 표시할 때 씁니다. 이 함수 정의는 실제론 잘못되었죠.test!(C, A, B)
라고 정의했어야 합니다. -
test3
test3(C, A, B) 40.831 ns (1 allocation: 128 bytes) CodeInfo( 1 ─ %1 = Base.broadcasted(Main.:+, A, B) │ %2 = Base.materialize(%1) │ Base.setindex!(C, %2, Main.:(:)) └── return Main.nothing )
이는
A .+ B
를 계산하고 이를C[:]
에 대입했다는 것입니다. Julia는 Functional Programming도 들어가있습니다. FP는 모든 것은 함수로 이루어졌다고 봅니다. 그 중 Array Indexing은 불러올때는getindex
를 대입할 때는setindex!
라는 함수를 호출한다고 생각하시면 됩니다. 즉 여기선,test1
과 비슷한 동작을 했지만, 대신 이건[:]
를 통해 다시 C에 대입합니다. 이러면 당연히test2
보다 allocation이 한 번 더 일어나므로 비효율적이죠. 그리고 이 함수 또한test3!(C, A, B)
라고 정의했어야합니다.
질문하신건 설명을 다 드린 것 같고 다른걸 테스트해보다가 재밌는걸 발견했습니다. @btime
으로 테스트해보니 제일 효율적인건
function test4(C, A, B)
for i=1:length(C)
C[i] = A[i] + B[i]
end
end
7.357 ns (0 allocations: 0 bytes)
더군요. test4
가 빠르다는건 예상했는데 broadcast
보다 두배나 더 빠를 줄은 몰랐네요. 아마 Type Inference과정때문인거 같은데 이건 한번 타입 지정해보시고 알아보시죠 ㅋㅋ (실은 귀찮아서..)
@btime
은 한번 돌려서 저렇게 되는건가 싶어서@benchmark
로 돌려봤는데 test4같은 경우에는 사이즈가 커질 수록 오래 걸리네요. 아마 작을때는 broadcast를 fuse하는 overhead가 살짝있지만 scale이 커지면서 broadcast가 훨씬 더 효율적으로 작동하는 것 같습니다.