Performance Tips

Aşağıdaki bölümlerde, Julia kodunuzun mümkün olan en hızlı şekilde çalışmasına yardımcı olabilecek birkaç tekniği kısaca gözden geçireceğiz.

Performance critical code should be inside a function

Herhangi bir performans açısından kritik olan kod bir fonksiyonun içinde olmalıdır. Fonksiyonların içindeki kod, Julia'nın derleyicisinin çalışma şekli nedeniyle genellikle üst düzey koddan çok daha hızlı çalışır.

Fonksiyonların kullanımı yalnızca performans için önemli değildir: fonksiyonlar daha yeniden kullanılabilir ve test edilebilir, ayrıca hangi adımların yapıldığını ve bunların girdileri ile çıktılarının ne olduğunu netleştirir, Write functions, not just scripts ayrıca Julia'nın Stil Kılavuzu'nda bir öneridir.

Fonksiyonlar, doğrudan küresel değişkenler üzerinde işlem yapmak yerine argümanlar almalıdır, bir sonraki noktaya bakın.

Avoid untyped global variables

Bir tür belirtilmemiş global değişkenin değeri herhangi bir noktada değişebilir, bu da türünün değişmesine yol açabilir. Bu, derleyicinin global değişkenler kullanan kodu optimize etmesini zorlaştırır. Bu, tür değerli değişkenler için de geçerlidir, yani global düzeydeki tür takma adları. Değişkenler mümkün olduğunca yerel olmalı veya fonksiyonlara argüman olarak geçirilmelidir.

Küresel adların sıklıkla sabitler olduğunu ve bunları bu şekilde tanımlamanın performansı büyük ölçüde artırdığını buluyoruz:

const DEFAULT_VAL = 0

Eğer bir global her zaman aynı türde biliniyorsa, the type should be annotated.

Kullanılmamış global değişkenlerin türleri, kullanıldığı noktada türlerini belirterek optimize edilebilir:

global x = rand(1000)

function loop_over_global()
    s = 0.0
    for i in x::Vector{Float64}
        s += i
    end
    return s
end

Fonksiyonlara argüman geçmek daha iyi bir stildir. Bu, daha yeniden kullanılabilir kodlar oluşturur ve girdilerin ve çıktının ne olduğunu netleştirir.

Note

Tüm kod REPL'de global kapsamda değerlendirilir, bu nedenle en üst düzeyde tanımlanan ve atanan bir değişken global bir değişken olacaktır. Modüller içinde en üst düzey kapsamda tanımlanan değişkenler de globaldir.

REPL oturumunda:

julia> x = 1.0

eşittir:

julia> global x = 1.0

bu nedenle daha önce tartışılan tüm performans sorunları geçerlidir.

Measure performance with @time and pay attention to memory allocation

Performans ölçmek için yararlı bir araç, @time makrosudur. Burada yukarıdaki küresel değişkenle örneği tekrar ediyoruz, ancak bu sefer tür açıklaması kaldırılmıştır:

julia> x = rand(1000);

julia> function sum_global()
           s = 0.0
           for i in x
               s += i
           end
           return s
       end;

julia> @time sum_global()
  0.011539 seconds (9.08 k allocations: 373.386 KiB, 98.69% compilation time)
523.0007221951678

julia> @time sum_global()
  0.000091 seconds (3.49 k allocations: 70.156 KiB)
523.0007221951678

İlk çağrıda (@time sum_global()) fonksiyon derlenir. (Eğer bu oturumda @time'yı henüz kullanmadıysanız, zamanlama için gerekli olan fonksiyonlar da derlenecektir.) Bu çalışmanın sonuçlarını ciddiye almamalısınız. İkinci çalışmada, zamanın raporlanmasının yanı sıra, önemli miktarda bellek tahsis edildiğini de belirtmiştir. Burada sadece 64-bit float'lardan oluşan bir vektördeki tüm elemanların toplamını hesaplıyoruz, bu nedenle (yığın) bellek tahsis etmeye gerek olmamalıdır.

@time raporlarının özellikle yığın tahsisatlarını bildirdiğini netleştirmeliyiz; bu genellikle değişken nesneler veya değişken boyutlu konteynerler (örneğin Array veya Dict, dizeler veya yalnızca çalışma zamanında bilinen "tip-istikrarsız" nesneler) oluşturmak/büyütmek için gereklidir. Bu tür bellek bloklarını tahsis etmek (veya serbest bırakmak) libc'ye (örneğin C'de malloc aracılığıyla) pahalı bir işlev çağrısı gerektirebilir ve bunlar çöp toplama için izlenmelidir. Buna karşılık, sayılar (bignum'lar hariç), demetler ve değişmez structlar gibi değişmez değerler çok daha ucuz bir şekilde saklanabilir; örneğin yığın veya CPU kayıt belleğinde, bu nedenle "tahsis etme" performans maliyeti hakkında genellikle endişelenmezsiniz.

Beklenmedik bellek tahsisi genellikle kodunuzda bir sorun olduğunun işareti olup, genellikle tür kararlılığı ile veya birçok küçük geçici dizi oluşturma ile ilgili bir sorundur. Sonuç olarak, tahsisin yanı sıra, fonksiyonunuz için üretilen kodun da oldukça optimal olmaması muhtemeldir. Bu tür belirtileri ciddiye alın ve aşağıdaki tavsiyeleri takip edin.

Bu özel durumda, bellek tahsisi, x adlı tür-istikrarsız bir global değişkenin kullanımından kaynaklanmaktadır, bu nedenle eğer x'i fonksiyona bir argüman olarak geçirirsek, artık bellek tahsis etmez (aşağıda rapor edilen kalan tahsis, global kapsamda @time makrosunun çalıştırılmasından kaynaklanmaktadır) ve ilk çağrıdan sonra önemli ölçüde daha hızlıdır:

julia> x = rand(1000);

julia> function sum_arg(x)
           s = 0.0
           for i in x
               s += i
           end
           return s
       end;

julia> @time sum_arg(x)
  0.007551 seconds (3.98 k allocations: 200.548 KiB, 99.77% compilation time)
523.0007221951678

julia> @time sum_arg(x)
  0.000006 seconds (1 allocation: 16 bytes)
523.0007221951678

Görülen 1 tahsis, @time makrosunun kendisinin global kapsamda çalıştırılmasından kaynaklanmaktadır. Eğer zamanlamayı bir fonksiyonda çalıştırırsak, gerçekten de hiçbir tahsisin yapılmadığını görebiliriz:

julia> time_sum(x) = @time sum_arg(x);

julia> time_sum(x)
  0.000002 seconds
523.0007221951678

Bazı durumlarda, işlevinizin çalışması sırasında bellek ayırması gerekebilir ve bu, yukarıdaki basit resmi karmaşıklaştırabilir. Bu tür durumlarda, sorunları teşhis etmek için aşağıdaki tools'lerden birini kullanmayı veya bellek ayırmayı algoritmik yönlerinden ayıran bir işlev sürümü yazmayı düşünün (bkz. Pre-allocating outputs).

Note

Daha ciddi kıyaslamalar için, gürültüyü azaltmak amacıyla fonksiyonu birden fazla kez değerlendiren BenchmarkTools.jl paketini dikkate almayı düşünün.

Tools

Julia ve paket ekosistemi, sorunları teşhis etmenize ve kodunuzun performansını artırmanıza yardımcı olabilecek araçlar içerir:

  • Profiling kodunuzun performansını ölçmenizi ve darboğaz oluşturan satırları tanımlamanızı sağlar. Karmaşık projeler için, ProfileView paketi profil sonuçlarınızı görselleştirmenize yardımcı olabilir.
  • JET paketi, kodunuzdaki yaygın performans sorunlarını bulmanıza yardımcı olabilir.
  • Beklenmedik şekilde büyük bellek tahsisleri – @time, @allocated veya profil aracılığıyla (çöp toplama rutinlerine yapılan çağrılarla) bildirildiği gibi – kodunuzda sorun olabileceğine işaret eder. Tahsisler için başka bir neden görmüyorsanız, bir tür problemi olduğundan şüphelenin. Ayrıca, Julia'yı --track-allocation=user seçeneğiyle başlatabilir ve bu tahsislerin nerede gerçekleştiğine dair bilgileri görmek için oluşan *.mem dosyalarını inceleyebilirsiniz. Memory allocation analysis'ya bakın.
  • @code_warntype kodunuzun, tür belirsizliğine yol açan ifadeleri bulmanıza yardımcı olabilecek bir temsilini oluşturur. Aşağıda @code_warntype'yı görebilirsiniz.

Avoid containers with abstract type parameters

Parametreli türlerle, diziler de dahil olmak üzere çalışırken, mümkünse soyut türlerle parametre vermekten kaçınmak en iyisidir.

Aşağıdakileri dikkate alın:

julia> a = Real[]
Real[]

julia> push!(a, 1); push!(a, 2.0); push!(a, π)
3-element Vector{Real}:
 1
 2.0
 π = 3.1415926535897...

Çünkü a, soyut türde bir dizi olan Real olduğundan, herhangi bir Gerçek değerini tutabilmelidir. Gerçek nesneleri keyfi boyut ve yapıda olabileceğinden, a'nın bireysel olarak tahsis edilmiş Gerçek nesnelerine işaret eden bir dizi olarak temsil edilmesi gerekir. Ancak, eğer a'da yalnızca aynı türde sayılara, örneğin Float64, izin veriyorsak, bunlar daha verimli bir şekilde saklanabilir:

julia> a = Float64[]
Float64[]

julia> push!(a, 1); push!(a, 2.0); push!(a,  π)
3-element Vector{Float64}:
 1.0
 2.0
 3.141592653589793

a'ya sayılar atamak artık bunları Float64'e dönüştürecek ve a, verimli bir şekilde işlenebilen 64-bit kayan nokta değerlerinin ardışık bir bloğu olarak saklanacaktır.

Eğer soyut değer türleriyle konteynerlerden kaçınamıyorsanız, bazen çalışma zamanı tür kontrolünden kaçınmak için Any ile parametre vermek daha iyidir. Örneğin, IdDict{Any, Any} IdDict{Type, Vector}'dan daha iyi performans gösterir.

Ayrıca Parametric Types altındaki tartışmaya bakın.

Type declarations

Birçok dilde isteğe bağlı tür bildirimleri ile, bildirim eklemek kodun daha hızlı çalışmasını sağlamak için ana yoldur. Bu, Julia'da böyle değildir. Julia'da derleyici genellikle tüm fonksiyon argümanlarının, yerel değişkenlerin ve ifadelerin türlerini bilir. Ancak, bildirimlerin faydalı olduğu birkaç özel durum vardır.

Avoid fields with abstract type

Alanlarının türlerini belirtmeden türler tanımlanabilir:

julia> struct MyAmbiguousType
           a
       end

Bu, a'nın herhangi bir türde olmasına izin verir. Bu genellikle faydalı olabilir, ancak bir dezavantajı vardır: MyAmbiguousType türündeki nesneler için derleyici yüksek performanslı kod üretemez. Bunun nedeni, derleyicinin nesnelerin değerlerini değil, türlerini kullanarak kodu nasıl oluşturacağını belirlemesidir. Ne yazık ki, MyAmbiguousType türündeki bir nesne hakkında çok az şey çıkarılabilir:

julia> b = MyAmbiguousType("Hello")
MyAmbiguousType("Hello")

julia> c = MyAmbiguousType(17)
MyAmbiguousType(17)

julia> typeof(b)
MyAmbiguousType

julia> typeof(c)
MyAmbiguousType

b ve c değerleri aynı türde olmasına rağmen, bellek içindeki veri temsilleri çok farklıdır. a alanında sadece sayısal değerler saklasanız bile, UInt8'nın bellek temsili ile Float64'nın bellek temsili arasındaki fark, CPU'nun bunları iki farklı türde talimatla işlemesi gerektiği anlamına gelir. Gerekli bilgiler türde mevcut olmadığından, bu tür kararlar çalışma zamanında alınmak zorundadır. Bu da performansı yavaşlatır.

a'n türünü belirtmekle daha iyi yapabilirsiniz. Burada, a'nın birkaç türden biri olabileceği duruma odaklanıyoruz; bu durumda doğal çözüm parametreleri kullanmaktır. Örneğin:

julia> mutable struct MyType{T<:AbstractFloat}
           a::T
       end

Bu, daha iyi bir seçimdir.

julia> mutable struct MyStillAmbiguousType
           a::AbstractFloat
       end

çünkü ilk sürüm, a'nın türünü sarıcı nesnenin türünden belirler. Örneğin:

julia> m = MyType(3.2)
MyType{Float64}(3.2)

julia> t = MyStillAmbiguousType(3.2)
MyStillAmbiguousType(3.2)

julia> typeof(m)
MyType{Float64}

julia> typeof(t)
MyStillAmbiguousType

a alanının türü m'nin türünden kolayca belirlenebilir, ancak t'nin türünden belirlenemez. Gerçekten de, t'de a alanının türünü değiştirmek mümkündür:

julia> typeof(t.a)
Float64

julia> t.a = 4.5f0
4.5f0

julia> typeof(t.a)
Float32

Buna karşın, m oluşturulduktan sonra m.a'nın tipi değiştirilemez:

julia> m.a = 4.5f0
4.5f0

julia> typeof(m.a)
Float64

m'nin türünden m.a'nın türünün biliniyor olması - bununla birlikte, türünün işlev ortasında değişmeyeceği gerçeği - derleyicinin m gibi nesneler için yüksek derecede optimize edilmiş kod üretmesine, ancak t gibi nesneler için üretememesine olanak tanır.

Tabii ki, bunların hepsi yalnızca m'yi somut bir türle inşa edersek doğrudur. Bunu, onu soyut bir türle açıkça inşa ederek bozabiliriz:

julia> m = MyType{AbstractFloat}(3.2)
MyType{AbstractFloat}(3.2)

julia> typeof(m.a)
Float64

julia> m.a = 4.5f0
4.5f0

julia> typeof(m.a)
Float32

Pratik amaçlar için, bu tür nesneler MyStillAmbiguousType nesneleriyle aynı şekilde davranır.

Basit bir fonksiyon için üretilen kod miktarını karşılaştırmak oldukça öğreticidir.

func(m::MyType) = m.a+1

kullanarak

code_llvm(func, Tuple{MyType{Float64}})
code_llvm(func, Tuple{MyType{AbstractFloat}})

Uzunluk nedenleriyle sonuçlar burada gösterilmemektedir, ancak bunu kendiniz denemek isteyebilirsiniz. İlk durumda tür tamamen belirtilmiş olduğundan, derleyicinin çalışma zamanında türü çözmek için herhangi bir kod üretmesine gerek yoktur. Bu, daha kısa ve daha hızlı kod ile sonuçlanır.

Birinin, tam olarak parametreleştirilmemiş türlerin soyut türler gibi davrandığını da aklında bulundurması gerekir. Örneğin, tam olarak belirtilmiş bir Array{T,n} somut olmasına rağmen, hiçbir parametre verilmeden Array kendisi somut değildir:

julia> !isconcretetype(Array), !isabstracttype(Array), isstructtype(Array), !isconcretetype(Array{Int}), isconcretetype(Array{Int,1})
(true, true, true, true, true)

Bu durumda, MyType'i a::Array ile bir alan olarak tanımlamaktan kaçınmak ve bunun yerine alanı a::Array{T,N} veya a::A olarak tanımlamak daha iyi olur; burada {T,N} veya A, MyType'in parametreleridir.

Önceki tavsiye, bir yapının alanlarının işlevler veya daha genel olarak çağrılabilir nesneler olarak tasarlandığı durumlarda özellikle faydalıdır. Bir yapıyı aşağıdaki gibi tanımlamak çok caziptir:

struct MyCallableWrapper
    f::Function
end

Ama Function soyut bir tür olduğu için, wrapper.f çağrısının her biri, f alanına erişimin tür kararsızlığı nedeniyle dinamik yönlendirme gerektirecektir. Bunun yerine, şöyle bir şey yazmalısınız:

struct MyCallableWrapper{F}
    f::F
end

hangi neredeyse aynı davranışa sahip ancak çok daha hızlı olacaktır (çünkü tür belirsizliği ortadan kaldırılmıştır). F<:Function koşulunu koymadığımızı unutmayın: bu, Function alt türü olmayan çağrılabilir nesnelerin de f alanı için izin verildiği anlamına gelir.

Avoid fields with abstract containers

Konteyner türleri için de aynı en iyi uygulamalar geçerlidir:

julia> struct MySimpleContainer{A<:AbstractVector}
           a::A
       end

julia> struct MyAmbiguousContainer{T}
           a::AbstractVector{T}
       end

julia> struct MyAlsoAmbiguousContainer
           a::Array
       end

Örneğin:

julia> c = MySimpleContainer(1:3);

julia> typeof(c)
MySimpleContainer{UnitRange{Int64}}

julia> c = MySimpleContainer([1:3;]);

julia> typeof(c)
MySimpleContainer{Vector{Int64}}

julia> b = MyAmbiguousContainer(1:3);

julia> typeof(b)
MyAmbiguousContainer{Int64}

julia> b = MyAmbiguousContainer([1:3;]);

julia> typeof(b)
MyAmbiguousContainer{Int64}

julia> d = MyAlsoAmbiguousContainer(1:3);

julia> typeof(d), typeof(d.a)
(MyAlsoAmbiguousContainer, Vector{Int64})

julia> d = MyAlsoAmbiguousContainer(1:1.0:3);

julia> typeof(d), typeof(d.a)
(MyAlsoAmbiguousContainer, Vector{Float64})

MySimpleContainer için, nesne türü ve parametreleri ile tamamen tanımlanmıştır, bu nedenle derleyici optimize edilmiş fonksiyonlar üretebilir. Çoğu durumda, bu muhtemelen yeterli olacaktır.

Derleyici artık işini mükemmel bir şekilde yapabiliyorken, a'nın eleman türüne bağlı olarak kodunuzun farklı şeyler yapmasını isteyebileceğiniz durumlar vardır. Genellikle bunu başarmanın en iyi yolu, belirli işleminizi (burada, foo) ayrı bir işlevin içine sarmaktır:

julia> function sumfoo(c::MySimpleContainer)
           s = 0
           for x in c.a
               s += foo(x)
           end
           s
       end
sumfoo (generic function with 1 method)

julia> foo(x::Integer) = x
foo (generic function with 1 method)

julia> foo(x::AbstractFloat) = round(x)
foo (generic function with 2 methods)

Bu, her durumda derleyicinin optimize edilmiş kod üretmesine izin verirken işleri basit tutar.

Ancak, MySimpleContainer içindeki a alanının farklı eleman türleri veya AbstractVector türleri için dış işlevin farklı sürümlerini tanımlamanız gereken durumlar vardır. Bunu şu şekilde yapabilirsiniz:

julia> function myfunc(c::MySimpleContainer{<:AbstractArray{<:Integer}})
           return c.a[1]+1
       end
myfunc (generic function with 1 method)

julia> function myfunc(c::MySimpleContainer{<:AbstractArray{<:AbstractFloat}})
           return c.a[1]+2
       end
myfunc (generic function with 2 methods)

julia> function myfunc(c::MySimpleContainer{Vector{T}}) where T <: Integer
           return c.a[1]+3
       end
myfunc (generic function with 3 methods)
julia> myfunc(MySimpleContainer(1:3))
2

julia> myfunc(MySimpleContainer(1.0:3))
3.0

julia> myfunc(MySimpleContainer([1:3;]))
4

Annotate values taken from untyped locations

Veri yapılarıyla çalışmak genellikle herhangi bir türde değerler içerebilen yapılarla ( Array{Any} türünde diziler) çalışmak için uygundur. Ancak, bu yapılardan birini kullanıyorsanız ve bir öğenin türünü biliyorsanız, bu bilgiyi derleyiciyle paylaşmak faydalıdır:

function foo(a::Array{Any,1})
    x = a[1]::Int32
    b = x+1
    ...
end

Burada, a'nın ilk elemanının Int32 olacağını bilme şansımız oldu. Bu şekilde bir not eklemenin ek bir avantajı, eğer değer beklenen türde değilse bir çalışma zamanı hatası oluşturmasıdır, bu da belirli hataları daha erken yakalama potansiyeli taşır.

a[1] türü tam olarak bilinmediğinde, x şu şekilde tanımlanabilir: x = convert(Int32, a[1])::Int32. convert fonksiyonunun kullanımı, a[1]'in Int32'ye dönüştürülebilen herhangi bir nesne olmasına (örneğin UInt8) olanak tanır ve böylece kodun genel yapısını artırarak tür gereksinimini gevşetir. Bu bağlamda convert'in kendisinin tür istikrarını sağlamak için bir tür açıklamasına ihtiyaç duyduğunu unutmayın. Bunun nedeni, derleyicinin bir fonksiyonun dönüş değerinin türünü, hatta convert'in bile, fonksiyonun tüm argümanlarının türleri bilinmediği sürece çıkaramamış olmasıdır.

Tip anotasyonu, tür soyut olduğunda veya çalışma zamanında oluşturulduğunda performansı artırmayacak (ve aslında engelleyebilir). Bunun nedeni, derleyicinin anotasyonu kullanarak sonraki kodu özelleştirememesidir ve tür kontrolü kendisi zaman alır. Örneğin, kodda:

function nr(a, prec)
    ctype = prec == 32 ? Float32 : Float64
    b = Complex{ctype}(a)
    c = (b + 1.0f0)::Complex{ctype}
    abs(c)
end

c anotasyonu performansı olumsuz etkiler. Çalışma zamanında oluşturulan türlerle ilgili performanslı kod yazmak için aşağıda tartışılan function-barrier technique'i kullanın ve oluşturulan türün, çekirdek işlevinin argüman türleri arasında yer aldığından emin olun, böylece çekirdek işlemleri derleyici tarafından uygun şekilde özelleştirilir. Örneğin, yukarıdaki kod parçasında, b oluşturulduğu anda, başka bir işlev k'ye, çekirdek işlevine, geçirilebilir. Örneğin, k işlevi b'yi Complex{T} türünde bir argüman olarak tanımlıyorsa, burada T bir tür parametresidir, o zaman k içindeki bir atama ifadesinde aşağıdaki biçimde bir tür anotasyonu ortaya çıkar:

c = (b + 1.0f0)::Complex{T}

performansı engellemez (ama yardımcı da olmaz) çünkü derleyici k derlenirken c'nin türünü belirleyebilir.

Be aware of when Julia avoids specializing

Bir sezgisel olarak, Julia Type, Function ve Vararg olmak üzere üç belirli durumda otomatik olarak specializing'ı argüman tür parametrelerinde kullanmaktan kaçınır. Julia, argüman metod içinde kullanıldığında her zaman uzmanlaşır, ancak argüman başka bir işleve sadece iletildiğinde uzmanlaşmaz. Bu genellikle çalışma zamanında hiçbir performans etkisi yaratmaz ve improves compiler performance'tadır. Eğer sizin durumunuzda çalışma zamanında bir performans etkisi olduğunu bulursanız, uzmanlaşmayı tetiklemek için yöntem bildirimine bir tür parametresi ekleyebilirsiniz. İşte bazı örnekler:

Bu, uzmanlaşmayacak:

function f_type(t)  # or t::Type
    x = ones(t, 10)
    return sum(map(sin, x))
end

ama bu şöyle olacak:

function g_type(t::Type{T}) where T
    x = ones(T, 10)
    return sum(map(sin, x))
end

Bunlar uzmanlaşmayacak:

f_func(f, num) = ntuple(f, div(num, 2))
g_func(g::Function, num) = ntuple(g, div(num, 2))

ama bu:

h_func(h::H, num) where {H} = ntuple(h, div(num, 2))

Bu, uzmanlaşmayacak:

f_vararg(x::Int...) = tuple(x...)

ama bu şöyle olacak:

g_vararg(x::Vararg{Int, N}) where {N} = tuple(x...)

Bir tek tür parametresi tanıtmak, diğer türler kısıtlanmamış olsa bile uzmanlaşmayı zorlamak için yeterlidir. Örneğin, bu da uzmanlaşacak ve argümanlar aynı türde olmadığında faydalıdır:

h_vararg(x::Vararg{Any, N}) where {N} = tuple(x...)

Not edin ki @code_typed ve arkadaşları her zaman size özel kod gösterecektir, Julia bu yöntem çağrısını normalde özelleştirmese bile. Argüman türleri değiştiğinde özelleştirmelerin üretilip üretilmediğini görmek istiyorsanız method internals kontrol etmelisiniz, yani Base.specializations(@which f(...)) ifadesinin ilgili argüman için özelleştirmeleri içerip içermediğini kontrol etmelisiniz.

Break functions into multiple definitions

Bir fonksiyonu birçok küçük tanım olarak yazmak, derleyicinin en uygun kodu doğrudan çağırmasına veya hatta iç içe almasına olanak tanır.

İşte gerçekten birden fazla tanım olarak yazılması gereken bir "bileşik fonksiyon" örneği:

using LinearAlgebra

function mynorm(A)
    if isa(A, Vector)
        return sqrt(real(dot(A,A)))
    elseif isa(A, Matrix)
        return maximum(svdvals(A))
    else
        error("mynorm: invalid argument")
    end
end

Bu daha özlü ve verimli bir şekilde şöyle yazılabilir:

mynorm(x::Vector) = sqrt(real(dot(x, x)))
mynorm(A::Matrix) = maximum(svdvals(A))

Ancak, derleyicinin mynorm örneği olarak yazılan koddaki ölü dalları optimize etmede oldukça verimli olduğu belirtilmelidir.

Write "type-stable" functions

Bir işlevin her zaman aynı türde bir değer döndürmesini sağlamak mümkün olduğunda, bu faydalı olur. Aşağıdaki tanıma bakın:

pos(x) = x < 0 ? 0 : x

Bu masum görünüyor olsa da, sorun şu ki 0 bir tam sayıdır (tipi Int) ve x herhangi bir tipte olabilir. Dolayısıyla, x'in değerine bağlı olarak, bu fonksiyon iki farklı tipten bir değer döndürebilir. Bu davranış izin verilen bir durumdur ve bazı durumlarda istenebilir. Ancak, bu kolayca şu şekilde düzeltilebilir:

pos(x) = x < 0 ? zero(x) : x

Ayrıca oneunit fonksiyonu ve daha genel bir oftype(x, y) fonksiyonu bulunmaktadır; bu fonksiyon y'yi x'in türüne dönüştürür.

Avoid changing the type of a variable

Bir benzer "tip-istikrarı" sorunu, bir fonksiyon içinde tekrar tekrar kullanılan değişkenler için mevcuttur:

function foo()
    x = 1
    for i = 1:10
        x /= rand()
    end
    return x
end

Yerel değişken x bir tamsayı olarak başlar ve bir döngü iterasyonundan sonra / operatörünün sonucu olarak bir kayan nokta sayısına dönüşür. Bu, derleyicinin döngü gövdesini optimize etmesini daha zor hale getirir. Birkaç olası çözüm vardır:

  • x ile x = 1.0 olarak başlatın
  • x::Float64 = 1
  • x = oneunit(Float64) için açık bir dönüşüm kullanın.
  • İlk döngü yinelemesi ile başlatın, x = 1 / rand() olarak ayarlayın, ardından for i = 2:10 döngüsünü başlatın.

Separate kernel functions (aka, function barriers)

Birçok fonksiyon, bir dizi hazırlık çalışması yapma ve ardından temel bir hesaplama gerçekleştirmek için birçok yineleme yapma desenini takip eder. Mümkünse, bu temel hesaplamaları ayrı fonksiyonlara koymak iyi bir fikirdir. Örneğin, aşağıdaki uydurma fonksiyon, rastgele seçilmiş bir türde bir dizi döndürür:

julia> function strange_twos(n)
           a = Vector{rand(Bool) ? Int64 : Float64}(undef, n)
           for i = 1:n
               a[i] = 2
           end
           return a
       end;

julia> strange_twos(3)
3-element Vector{Int64}:
 2
 2
 2

Bu şöyle yazılmalıdır:

julia> function fill_twos!(a)
           for i = eachindex(a)
               a[i] = 2
           end
       end;

julia> function strange_twos(n)
           a = Vector{rand(Bool) ? Int64 : Float64}(undef, n)
           fill_twos!(a)
           return a
       end;

julia> strange_twos(3)
3-element Vector{Int64}:
 2
 2
 2

Julia'nın derleyicisi, işlev sınırlarında argüman türleri için kodu özelleştirir, bu nedenle orijinal uygulamada döngü sırasında a'nın türünü bilmez (çünkü rastgele seçilir). Bu nedenle, ikinci versiyon genellikle daha hızlıdır çünkü iç döngü, fill_twos!'un farklı a türleri için yeniden derlenebilir.

İkinci form genellikle daha iyi bir stil olup daha fazla kod yeniden kullanımına yol açabilir.

Bu desen, Julia Base'te birkaç yerde kullanılmaktadır. Örneğin, vcat ve hcat'e bakın abstractarray.jl, veya kendi fill_twos! fonksiyonumuzu yazmak yerine kullanabileceğimiz fill! fonksiyonu.

strange_twos gibi işlevler, belirsiz türdeki verilerle çalışırken ortaya çıkar; örneğin, tam sayılar, ondalık sayılar, dizeler veya başka bir şey içerebilecek bir giriş dosyasından yüklenen veriler.

Types with values-as-parameters

N boyutlu ve her eksende 3 boyutuna sahip bir dizi oluşturmak istiyorsanız, bu dizileri şu şekilde oluşturabilirsiniz:

julia> A = fill(5.0, (3, 3))
3×3 Matrix{Float64}:
 5.0  5.0  5.0
 5.0  5.0  5.0
 5.0  5.0  5.0

Bu yaklaşım çok iyi çalışıyor: derleyici, A'nın Array{Float64,2} olduğunu anlayabilir çünkü doldurma değerinin türünü (5.0::Float64) ve boyutunu ((3, 3)::NTuple{2,Int}) bilir. Bu, derleyicinin aynı işlevde A'nın gelecekteki herhangi bir kullanımına yönelik çok verimli kodlar üretebileceği anlamına gelir.

Ama şimdi, rastgele boyutlarda 3×3×... dizisi oluşturan bir fonksiyon yazmak istediğinizi varsayalım; bir fonksiyon yazma konusunda kendinizi zorlayabilirsiniz.

julia> function array3(fillval, N)
           fill(fillval, ntuple(d->3, N))
       end
array3 (generic function with 1 method)

julia> array3(5.0, 2)
3×3 Matrix{Float64}:
 5.0  5.0  5.0
 5.0  5.0  5.0
 5.0  5.0  5.0

Bu çalışıyor, ancak (@code_warntype array3(5.0, 2) kullanarak kendiniz doğrulayabilirsiniz) sorun, çıktı türünün çıkarılamamasıdır: N argümanı bir Int türünde bir değerdir ve tür çıkarımı önceden değerini tahmin edemez (ve edemez). Bu, bu fonksiyonun çıktısını kullanan kodun temkinli olması gerektiği anlamına gelir; A'nın her erişiminde türü kontrol etmesi gerekir; böyle bir kod çok yavaş olacaktır.

Şimdi, bu tür sorunları çözmenin çok iyi bir yolu, function-barrier technique kullanmaktır. Ancak, bazı durumlarda tür kararsızlığını tamamen ortadan kaldırmak isteyebilirsiniz. Bu tür durumlarda, bir yaklaşım, boyutları bir parametre olarak geçmektir, örneğin Val{T}() aracılığıyla (bkz. "Value types"):

julia> function array3(fillval, ::Val{N}) where N
           fill(fillval, ntuple(d->3, Val(N)))
       end
array3 (generic function with 1 method)

julia> array3(5.0, Val(2))
3×3 Matrix{Float64}:
 5.0  5.0  5.0
 5.0  5.0  5.0
 5.0  5.0  5.0

Julia, ntuple'ın ikinci parametre olarak bir Val{::Int} örneğini kabul eden özel bir versiyonuna sahiptir; N'yi bir tür parametresi olarak geçirerek, "değerini" derleyiciye bildirmiş olursunuz. Sonuç olarak, bu array3 versiyonu derleyicinin dönüş türünü tahmin etmesine olanak tanır.

Ancak, bu tür tekniklerin kullanımı oldukça ince olabilir. Örneğin, array3'ü bu şekilde bir fonksiyondan çağırmanız faydalı olmayacaktır:

function call_array3(fillval, n)
    A = array3(fillval, Val(n))
end

Burada, aynı sorunu tekrar yaratmışsınız: derleyici n'in ne olduğunu tahmin edemez, bu yüzden Val(n)'in tipini bilemez. Val'ı kullanmaya çalışmak, ancak bunu yanlış bir şekilde yapmak, birçok durumda performansı daha kötü hale getirebilir. (Yalnızca Val'ı fonksiyon engeli hilesiyle birleştirerek çekirdek fonksiyonunu daha verimli hale getirmeye çalıştığınız durumlarda, yukarıdaki gibi bir kod kullanılmalıdır.)

Doğru kullanımına bir örnek Val şöyle olacaktır:

function filter3(A::AbstractArray{T,N}) where {T,N}
    kernel = array3(1, Val(N))
    filter(A, kernel)
end

Bu örnekte, N bir parametre olarak geçildiği için "değeri" derleyiciye bilinir. Temelde, Val(T) yalnızca T ya sabit/kodlanmış (Val(3)) ya da zaten tür alanında belirtilmiş olduğunda çalışır.

The dangers of abusing multiple dispatch (aka, more on types with values-as-parameters)

Birisi çoklu dağıtımı takdir etmeyi öğrendiğinde, her şey için bunu kullanma eğilimi anlaşılabilir bir şekilde aşırıya kaçabilir. Örneğin, bilgileri depolamak için bunu kullanmayı hayal edebilirsiniz, örneğin:

struct Car{Make, Model}
    year::Int
    ...more fields...
end

ve sonra Car{:Honda,:Accord}(year, args...) gibi nesneler üzerinde dağıtım yapın.

Bu, aşağıdakilerden herhangi biri doğru olduğunda faydalı olabilir:

  • Her Car üzerinde CPU yoğun işlem gerektiriyorsanız ve Make ile Model'i derleme zamanında bilmek çok daha verimli hale geliyorsa, kullanılacak farklı Make veya Model sayısının çok büyük olmaması gerekir.
  • Homojen Car türünde listeleriniz var, böylece hepsini Array{Car{:Honda,:Accord},N} içinde depolayabilirsiniz.

Son durumda, böyle bir homojen diziyi işleyen bir fonksiyon verimli bir şekilde özelleştirilebilir: Julia, her bir elemanın türünü önceden bilir (konteynerdeki tüm nesneler aynı somut türe sahiptir), bu nedenle Julia, fonksiyon derlenirken doğru yöntem çağrılarını "arama" yapabilir (çalışma zamanında kontrol etme ihtiyacını ortadan kaldırarak) ve böylece tüm listeyi işlemek için verimli kod üretebilir.

Bu durumlar geçerli olmadığında, muhtemelen hiçbir fayda elde edemeyeceksiniz; daha kötüsü, ortaya çıkan "tiplerin kombinatoryal patlaması" ters etki yapacaktır. Eğer items[i+1] ile item[i] farklı bir tipe sahipse, Julia çalışma zamanında tipi kontrol etmek, uygun yöntemi yöntem tablolarında aramak, hangi yöntemin eşleştiğini (tip kesişimi aracılığıyla) belirlemek, henüz JIT derlenip derlenmediğini tespit etmek (eğer derlenmemişse bunu yapmak) ve ardından çağrıyı yapmak zorundadır. Özünde, tam tip sistemi ve JIT derleme mekanizmasını, kendi kodunuzda bir switch ifadesi veya sözlük araması gerçekleştirmesi için zorlamış oluyorsunuz.

Bazı çalışma zamanı karşılaştırmaları (1) tür dağıtımı, (2) sözlük araması ve (3) bir "switch" ifadesi ile ilgili olarak on the mailing list.

Belki de çalışma zamanı etkisinden daha kötü olan derleme zamanı etkisidir: Julia, her farklı Car{Make, Model} için özel işlevler derleyecektir; eğer yüzlerce veya binlerce böyle türünüz varsa, o zaman böyle bir nesneyi parametre olarak kabul eden her işlev (kendi yazabileceğiniz özel get_year işlevinden, Julia Temelindeki genel push! işlevine kadar) için yüzlerce veya binlerce varyant derlenecektir. Bunların her biri, derlenmiş kodun önbellek boyutunu, yöntemlerin iç listelerinin uzunluğunu vb. artırır. Değerleri parametre olarak kullanma konusundaki aşırı heves, kolayca muazzam kaynakları israf edebilir.

Access arrays in memory order, along columns

Julia'da çok boyutlu diziler sütun öncelikli sırada saklanır. Bu, dizilerin bir sütun bir seferde yığıldığı anlamına gelir. Bu, vec fonksiyonu veya aşağıda gösterildiği gibi [:] sözdizimi kullanılarak doğrulanabilir (dizinin [1 3 2 4] sırasına göre düzenlendiğini, [1 2 3 4] değil, dikkat edin):

julia> x = [1 2; 3 4]
2×2 Matrix{Int64}:
 1  2
 3  4

julia> x[:]
4-element Vector{Int64}:
 1
 3
 2
 4

Bu dizileri sıralama konvansiyonu, Fortran, Matlab ve R gibi birçok dilde yaygındır (birkaçını saymak gerekirse). Sütun-temelli sıralamanın alternatifi, C ve Python (numpy) gibi diğer dillerin benimsediği satır-temelli sıralamadır. Dizilerin sıralamasını hatırlamak, diziler üzerinde döngü kurarken önemli performans etkileri yaratabilir. Akılda tutulması gereken bir kural, sütun-temelli dizilerde ilk indeksin en hızlı değiştiğidir. Temelde bu, en içteki döngü indeksinin bir dilim ifadesinde ilk olarak görünmesi durumunda döngünün daha hızlı olacağı anlamına gelir. : ile bir diziyi indekslemenin, belirli bir boyuttaki tüm elemanlara iteratif olarak erişen örtük bir döngü olduğunu unutmayın; örneğin, sütunları çıkarmak satırlardan daha hızlı olabilir.

Aşağıdaki uydurma örneği düşünün. Bir Vector kabul eden ve kare bir Matrix döndüren bir fonksiyon yazmak istediğimizi hayal edin; bu kare, ya satırlar ya da sütunlar, girdi vektörünün kopyalarıyla doldurulmuş olsun. Bu kopyaların hangi şekilde doldurulduğunun önemli olmadığını varsayalım (belki de kodun geri kalanı buna göre kolayca uyarlanabilir). Bunu, yerleşik repeat çağrısına ek olarak en az dört farklı şekilde yapabiliriz:

function copy_cols(x::Vector{T}) where T
    inds = axes(x, 1)
    out = similar(Array{T}, inds, inds)
    for i = inds
        out[:, i] = x
    end
    return out
end

function copy_rows(x::Vector{T}) where T
    inds = axes(x, 1)
    out = similar(Array{T}, inds, inds)
    for i = inds
        out[i, :] = x
    end
    return out
end

function copy_col_row(x::Vector{T}) where T
    inds = axes(x, 1)
    out = similar(Array{T}, inds, inds)
    for col = inds, row = inds
        out[row, col] = x[row]
    end
    return out
end

function copy_row_col(x::Vector{T}) where T
    inds = axes(x, 1)
    out = similar(Array{T}, inds, inds)
    for row = inds, col = inds
        out[row, col] = x[col]
    end
    return out
end

Şimdi bu fonksiyonların her birini aynı rastgele 10000 x 1 girdi vektörü ile zamanlayacağız:

julia> x = randn(10000);

julia> fmt(f) = println(rpad(string(f)*": ", 14, ' '), @elapsed f(x))

julia> map(fmt, [copy_cols, copy_rows, copy_col_row, copy_row_col]);
copy_cols:    0.331706323
copy_rows:    1.799009911
copy_col_row: 0.415630047
copy_row_col: 1.721531501

copy_cols'un copy_rows'dan çok daha hızlı olduğunu unutmayın. Bu beklenen bir durumdur çünkü copy_cols, Matrix'in sütun bazlı bellek düzenini dikkate alır ve her seferinde bir sütun doldurur. Ayrıca, copy_col_row, copy_row_col'dan çok daha hızlıdır çünkü dilim ifadesinde görünen ilk öğenin en içteki döngü ile birleştirilmesi gerektiği kuralımıza uyar.

Pre-allocating outputs

Eğer fonksiyonunuz bir Array veya başka bir karmaşık tür döndürüyorsa, bellek ayırması gerekebilir. Ne yazık ki, genellikle bellek ayırma ve onun tersine, çöp toplama, önemli darboğazlar olabilir.

Bazen her fonksiyon çağrısında bellek ayırma ihtiyacını önceden ayırarak aşabilirsiniz. Basit bir örnek olarak, karşılaştırın

julia> function xinc(x)
           return [x, x+1, x+2]
       end;

julia> function loopinc()
           y = 0
           for i = 1:10^7
               ret = xinc(i)
               y += ret[2]
           end
           return y
       end;

ile

julia> function xinc!(ret::AbstractVector{T}, x::T) where T
           ret[1] = x
           ret[2] = x+1
           ret[3] = x+2
           nothing
       end;

julia> function loopinc_prealloc()
           ret = Vector{Int}(undef, 3)
           y = 0
           for i = 1:10^7
               xinc!(ret, i)
               y += ret[2]
           end
           return y
       end;

Zamanlama sonuçları:

julia> @time loopinc()
  0.529894 seconds (40.00 M allocations: 1.490 GiB, 12.14% gc time)
50000015000000

julia> @time loopinc_prealloc()
  0.030850 seconds (6 allocations: 288 bytes)
50000015000000

Önceden tahsis etmenin başka avantajları da vardır; örneğin, çağıranın bir algoritmadan "çıktı" türünü kontrol etmesine olanak tanır. Yukarıdaki örnekte, isterseniz SubArray yerine Array geçirebilirdik.

Aşırıya kaçıldığında, ön tahsis kodunuzu çirkinleştirebilir, bu nedenle performans ölçümleri ve bazı yargılar gerekebilir. Ancak, "vektörleştirilmiş" (eleman bazında) fonksiyonlar için, geçici diziler olmadan birleştirilmiş döngülerle yerinde işlemler için x .= f.(y) kullanışlı sözdizimi kullanılabilir (bkz. dot syntax for vectorizing functions).

Use MutableArithmetics for more control over allocation for mutable arithmetic types

Bazı Number alt türleri, BigInt veya BigFloat gibi, mutable struct türleri olarak uygulanabilir veya değişken bileşenlere sahip olabilir. Julia Base içindeki aritmetik arayüzler genellikle bu tür durumlarda verimlilikten çok kullanım kolaylığını tercih eder, bu nedenle bunları naif bir şekilde kullanmak alt optimal performansa yol açabilir. Öte yandan, MutableArithmetics paketinin soyutlamaları, bu türlerin değişkenliğinden yararlanarak yalnızca gerekli kadar bellek ayıran hızlı kod yazmayı mümkün kılar. MutableArithmetics, ayrıca gerektiğinde değişken aritmetik türlerinin değerlerini açıkça kopyalamayı da mümkün kılar. MutableArithmetics, bir kullanıcı paketidir ve Julia projesiyle bağlantılı değildir.

More dots: Fuse vectorized operations

Julia, özel bir dot syntax'e sahiptir; bu, herhangi bir skalar fonksiyonu "vektörleştirilmiş" bir fonksiyon çağrısına ve herhangi bir operatörü "vektörleştirilmiş" bir operatöre dönüştürür. Özel bir özellik olarak, iç içe "nokta çağrıları" birleştirilir: bunlar, geçici diziler ayırmadan, sözdizimi düzeyinde tek bir döngüde birleştirilir. .= ve benzeri atama operatörlerini kullanırsanız, sonuç önceden tahsis edilmiş bir dizide yerinde de saklanabilir (yukarıya bakın).

Bir lineer cebir bağlamında, bu, vector + vector ve vector * scalar gibi işlemler tanımlı olsa da, bunun yerine vector .+ vector ve vector .* scalar kullanmanın avantajlı olabileceği anlamına gelir çünkü elde edilen döngüler çevresindeki hesaplamalarla birleştirilebilir. Örneğin, iki fonksiyonu düşünün:

julia> f(x) = 3x.^2 + 4x + 7x.^3;

julia> fdot(x) = @. 3x^2 + 4x + 7x^3; # equivalent to 3 .* x.^2 .+ 4 .* x .+ 7 .* x.^3

Her iki f ve fdot aynı şeyi hesaplar. Ancak, bir diziye uygulandığında fdot ( @. makrosunun yardımıyla tanımlanmıştır) önemli ölçüde daha hızlıdır:

julia> x = rand(10^6);

julia> @time f(x);
  0.019049 seconds (16 allocations: 45.777 MiB, 18.59% gc time)

julia> @time fdot(x);
  0.002790 seconds (6 allocations: 7.630 MiB)

julia> @time f.(x);
  0.002626 seconds (8 allocations: 7.630 MiB)

Yani, fdot(x) on f(x)'in on katı hızında çalışır ve 1/6 oranında bellek ayırır, çünkü f(x)'teki her * ve + işlemi yeni bir geçici dizi ayırır ve ayrı bir döngüde çalışır. Bu örnekte f.(x) fdot(x) kadar hızlıdır, ancak birçok bağlamda, her vektörleştirilmiş işlem için ayrı bir fonksiyon tanımlamaktan ziyade ifadelerinize bazı noktalar eklemek daha kullanışlıdır.

Fewer dots: Unfuse certain intermediate broadcasts

Yukarıda bahsedilen nokta döngü birleştirmesi, yüksek performanslı işlemleri ifade etmek için özlü ve deyimsel bir kod yazmayı sağlar. Ancak, birleştirilmiş işlemin her yayılma yinelemesinde hesaplanacağını unutmamak önemlidir. Bu, bazı durumlarda, özellikle bileşenli veya çok boyutlu yayılmaların varlığında, nokta çağrıları içeren bir ifadenin istenenden daha fazla kez bir fonksiyonu hesaplıyor olabileceği anlamına gelir. Örneğin, satırlarının Öklid normunun bir olduğu rastgele bir matris oluşturmak istiyoruz. Aşağıdaki gibi bir şey yazabiliriz:

julia> x = rand(1000, 1000);

julia> d = sum(abs2, x; dims=2);

julia> @time x ./= sqrt.(d);
  0.002049 seconds (4 allocations: 96 bytes)

Bu işe yarayacak. Ancak, bu ifade aslında x[i, :] satırındaki her eleman için sqrt(d[i])'yi yeniden hesaplayacak, bu da gerekli olandan çok daha fazla karekök hesaplandığı anlamına geliyor. Yayılmanın hangi indeksler üzerinde döneceğini tam olarak görmek için, birleşik ifadenin argümanları üzerinde Broadcast.combine_axes çağrısı yapabiliriz. Bu, döngü eksenlerine karşılık gelen aralıkların bir demetini döndürecektir; bu aralıkların uzunluklarının çarpımı, birleşik işlemin toplam çağrı sayısı olacaktır.

Bunun sonucunda, yayın ifadesinin bazı bileşenleri bir eksen boyunca sabit olduğunda—önceki örnekte olduğu gibi ikinci boyut boyunca sqrt—bu bileşenleri zorla "birleştirmeden ayırma" yoluyla performans iyileştirme potansiyeli vardır; yani, yayınlanan işlemin sonucunu önceden tahsis etmek ve sabit eksen boyunca önbelleğe alınan değeri yeniden kullanmaktır. Bu tür potansiyel yaklaşımlardan bazıları geçici değişkenler kullanmak, nokta ifadesinin bileşenlerini identity ile sarmak veya eşdeğer bir içsel vektörleştirilmiş (ancak birleştirilmemiş) fonksiyon kullanmaktır.

julia> @time let s = sqrt.(d); x ./= s end;
  0.000809 seconds (5 allocations: 8.031 KiB)

julia> @time x ./= identity(sqrt.(d));
  0.000608 seconds (5 allocations: 8.031 KiB)

julia> @time x ./= map(sqrt, d);
  0.000611 seconds (4 allocations: 8.016 KiB)

Bu seçeneklerden herhangi biri, bir tahsisat maliyetiyle yaklaşık üç kat hız artışı sağlar; büyük yayınlanabilirler için bu hız artışı asimptotik olarak çok büyük olabilir.

Consider using views for slices

Julia'da bir dizi "dilim" ifadesi olan array[1:5, :], o verinin bir kopyasını oluşturur (atamanın sol tarafında array[1:5, :] = ... durumunda, o kısmına yerinde atama yapar). Eğer dilim üzerinde birçok işlem yapıyorsanız, bu performans için iyi olabilir çünkü daha küçük bir sürekli kopya ile çalışmak, orijinal diziye indekslemekten daha verimlidir. Öte yandan, eğer dilim üzerinde sadece birkaç basit işlem yapıyorsanız, tahsis ve kopyalama işlemlerinin maliyeti önemli olabilir.

Bir alternatif, dizinin bir "görünümünü" oluşturmaktır; bu, aslında orijinal dizinin verilerine yerinde referans veren bir dizi nesnesidir (bir SubArray), kopya oluşturmadan. (Bir görünümü yazarsanız, orijinal dizinin verilerini de değiştirir.) Bu, view çağrılarak bireysel dilimler için yapılabilir veya daha basit bir şekilde, o ifadenin önüne @views koyarak bir bütün ifade veya kod bloğu için yapılabilir. Örneğin:

julia> fcopy(x) = sum(x[2:end-1]);

julia> @views fview(x) = sum(x[2:end-1]);

julia> x = rand(10^6);

julia> @time fcopy(x);
  0.003051 seconds (3 allocations: 7.629 MB)

julia> @time fview(x);
  0.001020 seconds (1 allocation: 16 bytes)

fview sürümünün işlevinin hem 3× hız artışını hem de azalan bellek tahsisini dikkate alın.

Copying data is not always bad

Diziler, bellekte ardışık olarak saklanır ve bu da CPU vektörleştirmesine ve önbellek nedeniyle daha az bellek erişimine olanak tanır. Bu, dizilere sütun-birincil sırayla erişilmesi önerilen aynı nedenlerdir (yukarıya bakın). Düzensiz erişim desenleri ve ardışık olmayan görünümler, diziler üzerindeki hesaplamaları önemli ölçüde yavaşlatabilir çünkü ardışık olmayan bellek erişimi söz konusudur.

Düzensiz erişilen verileri tekrar erişimden önce bitişik bir diziye kopyalamak, aşağıdaki örnekte olduğu gibi büyük bir hız artışı sağlayabilir. Burada, bir matris rastgele karıştırılmış indekslerde erişilmektedir ve ardından çarpılmaktadır. Düz dizilere kopyalamak, kopyalama ve tahsisatın ek maliyetine rağmen çarpımı hızlandırır.

julia> using Random

julia> A = randn(3000, 3000);

julia> x = randn(2000);

julia> inds = shuffle(1:3000)[1:2000];

julia> function iterated_neural_network(A, x, depth)
           for _ in 1:depth
               x .= max.(0, A * x)
           end
           argmax(x)
       end

julia> @time iterated_neural_network(view(A, inds, inds), x, 10)
  0.324903 seconds (12 allocations: 157.562 KiB)
1569

julia> @time iterated_neural_network(A[inds, inds], x, 10)
  0.054576 seconds (13 allocations: 30.671 MiB, 13.33% gc time)
1569

Yeterli bellek mevcutsa, görünümün bir diziye kopyalanmasının maliyeti, ardışık bir dizide tekrarlanan matris çarpımlarını yapmanın sağladığı hız artışıyla karşılanır.

Consider StaticArrays.jl for small fixed-size vector/matrix operations

Eğer uygulamanız, sabit boyutlu (yani boyut yürütmeden önce biliniyor) birçok küçük (< 100 eleman) dizi içeriyorsa, StaticArrays.jl package paketini kullanmayı düşünebilirsiniz. Bu paket, gereksiz yığın tahsisatlarını önleyen ve derleyicinin dizinin boyutu için kodu özelleştirmesine olanak tanıyan bir şekilde bu dizileri temsil etmenizi sağlar; örneğin, vektör işlemlerini tamamen açarak (döngüleri ortadan kaldırarak) ve elemanları CPU kayıtlarında saklayarak.

Örneğin, 2D geometrilerle hesaplamalar yapıyorsanız, 2 bileşenli vektörlerle birçok hesaplamanız olabilir. StaticArrays.jl kütüphanesinden SVector türünü kullanarak, v ve w vektörleri üzerinde norm(3v - w) gibi kullanışlı vektör notasyonu ve işlemleri kullanabilirsiniz; bu, derleyicinin kodu, @inbounds hypot(3v[1]-w[1], 3v[2]-w[2]) ile eşdeğer minimal bir hesaplama için açmasına olanak tanır.

Avoid string interpolation for I/O

Veri bir dosyaya (veya başka bir I/O cihazına) yazarken, ekstra ara dizgiler oluşturmak bir yük kaynağıdır. Bunun yerine:

println(file, "$a $b")

kullanmak:

println(file, a, " ", b)

Kodun ilk versiyonu bir dize oluşturur, ardından bunu dosyaya yazar, ikinci versiyon ise değerleri doğrudan dosyaya yazar. Ayrıca, bazı durumlarda dize interpolasyonunun okunmasının daha zor olabileceğini unutmayın. Dikkate alın:

println(file, "$(f(a))$(f(b))")

karşısında:

println(file, f(a), f(b))

Optimize network I/O during parallel execution

Uzak bir işlevi paralel olarak yürütürken:

using Distributed

responses = Vector{Any}(undef, nworkers())
@sync begin
    for (idx, pid) in enumerate(workers())
        @async responses[idx] = remotecall_fetch(foo, pid, args...)
    end
end

daha hızlıdır:

using Distributed

refs = Vector{Any}(undef, nworkers())
for (idx, pid) in enumerate(workers())
    refs[idx] = @spawnat pid foo(args...)
end
responses = [fetch(r) for r in refs]

Eski sonuç, her işçi için tek bir ağ gidiş-dönüşüne yol açarken, diğeri iki ağ çağrısına neden olur - ilk olarak @spawnat tarafından ve ikincisi fetch nedeniyle (veya hatta wait). 4d61726b646f776e2e436f64652822222c202266657463682229_40726566/4d61726b646f776e2e436f64652822222c2022776169742229_40726566 de seri olarak yürütülmekte ve bu da genel olarak daha kötü bir performansa yol açmaktadır.

Fix deprecation warnings

Bir kullanımdan kaldırılmış işlev, yalnızca bir kez ilgili bir uyarı yazdırmak için dahili olarak bir arama gerçekleştirir. Bu ek arama, önemli bir yavaşlamaya neden olabilir, bu nedenle kullanımdan kaldırılmış işlevlerin tüm kullanımları, uyarılarda önerildiği gibi değiştirilmelidir.

Tweaks

Bunlar sıkı iç döngülerde yardımcı olabilecek bazı küçük noktalardır.

Performance Annotations

Bazen belirli program özelliklerini vaat ederek daha iyi optimizasyonu etkinleştirebilirsiniz.

  • @inbounds dizisini ifadeler içinde dizi sınır kontrolünü ortadan kaldırmak için kullanın. Bunu yapmadan önce emin olun. Eğer alt dizinler sınırların dışındaysa, çökme veya sessiz bozulmalar yaşayabilirsiniz.
  • @fastmath kullanarak gerçek sayılar için doğru olan, ancak IEEE sayıları için farklılıklara yol açan kayan nokta optimizasyonlarına izin verin. Bunu yaparken dikkatli olun, çünkü bu sayısal sonuçları değiştirebilir. Bu, clang'ın -ffast-math seçeneğine karşılık gelir.
  • @simd for loops to promise that the iterations are independent and may be reordered. Note that in many cases, Julia can automatically vectorize code without the @simd macro; it is only beneficial in cases where such a transformation would otherwise be illegal, including cases like allowing floating-point re-associativity and ignoring dependent memory accesses (@simd ivdep). Again, be very careful when asserting @simd as erroneously annotating a loop with dependent iterations may result in unexpected results. In particular, note that setindex! on some AbstractArray subtypes is inherently dependent upon iteration order. This feature is experimental and could change or disappear in future versions of Julia.

1:n kullanarak bir AbstractArray'ye indeksleme yapmak yaygın bir deyimdir, ancak Array alışılmadık indeksleme kullanıyorsa güvenli değildir ve sınır kontrolü kapatıldığında bir segmentasyon hatasına neden olabilir. Bunun yerine LinearIndices(x) veya eachindex(x) kullanın (ayrıca bkz. Arrays with custom indices).

Note

@simd en içteki for döngüsünün hemen önüne yerleştirilmesi gerekirken, hem @inbounds hem de @fastmath tekil ifadeler veya iç içe geçmiş kod bloklarında yer alan tüm ifadeler için uygulanabilir; örneğin, @inbounds begin veya @inbounds for ... kullanarak.

İşte hem @inbounds hem de @simd işaretlemesi ile bir örnek (burada, optimizasyoncunun çok zeki olmaya çalışmasını engellemek ve benchmark'ımızı boşa çıkarmamak için @noinline kullanıyoruz):

@noinline function inner(x, y)
    s = zero(eltype(x))
    for i=eachindex(x)
        @inbounds s += x[i]*y[i]
    end
    return s
end

@noinline function innersimd(x, y)
    s = zero(eltype(x))
    @simd for i = eachindex(x)
        @inbounds s += x[i] * y[i]
    end
    return s
end

function timeit(n, reps)
    x = rand(Float32, n)
    y = rand(Float32, n)
    s = zero(Float64)
    time = @elapsed for j in 1:reps
        s += inner(x, y)
    end
    println("GFlop/sec        = ", 2n*reps / time*1E-9)
    time = @elapsed for j in 1:reps
        s += innersimd(x, y)
    end
    println("GFlop/sec (SIMD) = ", 2n*reps / time*1E-9)
end

timeit(1000, 1000)

2.4GHz Intel Core i5 işlemcisine sahip bir bilgisayarda bu şunu üretir:

GFlop/sec        = 1.9467069505224963
GFlop/sec (SIMD) = 17.578554163920018

(GFlop/sani performansı ölçer ve daha büyük sayılar daha iyidir.)

İşte üç tür işaretlemenin bulunduğu bir örnek. Bu program önce bir boyutlu bir dizinin sonlu farkını hesaplar ve ardından sonucun L2-normunu değerlendirir:

function init!(u::Vector)
    n = length(u)
    dx = 1.0 / (n-1)
    @fastmath @inbounds @simd for i in 1:n #by asserting that `u` is a `Vector` we can assume it has 1-based indexing
        u[i] = sin(2pi*dx*i)
    end
end

function deriv!(u::Vector, du)
    n = length(u)
    dx = 1.0 / (n-1)
    @fastmath @inbounds du[1] = (u[2] - u[1]) / dx
    @fastmath @inbounds @simd for i in 2:n-1
        du[i] = (u[i+1] - u[i-1]) / (2*dx)
    end
    @fastmath @inbounds du[n] = (u[n] - u[n-1]) / dx
end

function mynorm(u::Vector)
    n = length(u)
    T = eltype(u)
    s = zero(T)
    @fastmath @inbounds @simd for i in 1:n
        s += u[i]^2
    end
    @fastmath @inbounds return sqrt(s)
end

function main()
    n = 2000
    u = Vector{Float64}(undef, n)
    init!(u)
    du = similar(u)

    deriv!(u, du)
    nu = mynorm(du)

    @time for i in 1:10^6
        deriv!(u, du)
        nu = mynorm(du)
    end

    println(nu)
end

main()

2.7 GHz Intel Core i7 işlemcisine sahip bir bilgisayarda bu şunu üretir:

$ julia wave.jl;
  1.207814709 seconds
4.443986180758249

$ julia --math-mode=ieee wave.jl;
  4.487083643 seconds
4.443986180758249

Burada, --math-mode=ieee seçeneği @fastmath makrosunu devre dışı bırakır, böylece sonuçları karşılaştırabiliriz.

Bu durumda, @fastmath nedeniyle hız artışı yaklaşık 3.7 katıdır. Bu alışılmadık derecede büyük – genel olarak, hız artışı daha küçük olacaktır. (Bu özel örnekte, benchmark'ın çalışma seti işlemcinin L1 önbelleğine sığacak kadar küçüktür, bu nedenle bellek erişim gecikmesi bir rol oynamaz ve hesaplama süresi CPU kullanımına bağlıdır. Birçok gerçek dünya programında bu durum böyle değildir.) Ayrıca, bu durumda bu optimizasyon sonucu değiştirmez – genel olarak, sonuç biraz farklı olacaktır. Bazı durumlarda, özellikle sayısal olarak kararsız algoritmalar için, sonuç çok farklı olabilir.

@fastmath anotasyonu, kayan nokta ifadelerini yeniden düzenler; örneğin, değerlendirme sırasını değiştirmek veya belirli özel durumların (inf, nan) meydana gelemeyeceğini varsaymak gibi. Bu durumda (ve bu özel bilgisayarda), deriv fonksiyonundaki 1 / (2*dx) ifadesi döngüden çıkarılır (yani döngü dışında hesaplanır), sanki idx = 1 / (2*dx) yazmış gibi. Döngüde, ... / (2*dx) ifadesi artık ... * idx haline gelir ki bu da değerlendirmek için çok daha hızlıdır. Elbette, derleyici tarafından uygulanan gerçek optimizasyon ile elde edilen hız artışı, donanıma çok bağlıdır. Oluşturulan koddaki değişikliği incelemek için Julia'nın code_native fonksiyonunu kullanabilirsiniz.

@fastmath ayrıca hesaplama sırasında NaN'lerin meydana gelmeyeceğini varsayar, bu da şaşırtıcı davranışlara yol açabilir:

julia> f(x) = isnan(x);

julia> f(NaN)
true

julia> f_fast(x) = @fastmath isnan(x);

julia> f_fast(NaN)
false

Treat Subnormal Numbers as Zeros

Subnormal sayılar, daha önce denormal numbers olarak adlandırılan, birçok bağlamda faydalıdır, ancak bazı donanımlarda performans cezası getirir. set_zero_subnormals(true) çağrısı, kayan nokta işlemlerinin subnormal giriş veya çıkışları sıfır olarak değerlendirmesine izin verir, bu da bazı donanımlarda performansı artırabilir. set_zero_subnormals(false) çağrısı, subnormal sayılar için katı IEEE davranışını zorlar.

Aşağıda, subnormal sayıların bazı donanımlarda performansı belirgin şekilde etkilediği bir örnek bulunmaktadır:

function timestep(b::Vector{T}, a::Vector{T}, Δt::T) where T
    @assert length(a)==length(b)
    n = length(b)
    b[1] = 1                            # Boundary condition
    for i=2:n-1
        b[i] = a[i] + (a[i-1] - T(2)*a[i] + a[i+1]) * Δt
    end
    b[n] = 0                            # Boundary condition
end

function heatflow(a::Vector{T}, nstep::Integer) where T
    b = similar(a)
    for t=1:div(nstep,2)                # Assume nstep is even
        timestep(b,a,T(0.1))
        timestep(a,b,T(0.1))
    end
end

heatflow(zeros(Float32,10),2)           # Force compilation
for trial=1:6
    a = zeros(Float32,1000)
    set_zero_subnormals(iseven(trial))  # Odd trials use strict IEEE arithmetic
    @time heatflow(a,1000)
end

Bu, aşağıdakine benzer bir çıktı verir

  0.002202 seconds (1 allocation: 4.063 KiB)
  0.001502 seconds (1 allocation: 4.063 KiB)
  0.002139 seconds (1 allocation: 4.063 KiB)
  0.001454 seconds (1 allocation: 4.063 KiB)
  0.002115 seconds (1 allocation: 4.063 KiB)
  0.001455 seconds (1 allocation: 4.063 KiB)

Her her bir çift yinelemenin önemli ölçüde daha hızlı olduğunu not edin.

Bu örnek, a içindeki değerlerin zamanla yavaşça düzleşen bir şekilde üssel olarak azalan bir eğri haline gelmesi nedeniyle birçok subnormal sayı üretir.

Subnormal sayıları sıfır olarak ele almak dikkatle kullanılmalıdır, çünkü bu bazı kimlikleri bozar, örneğin x-y == 0 ifadesi x == y anlamına gelir:

julia> x = 3f-38; y = 2f-38;

julia> set_zero_subnormals(true); (x - y, x == y)
(0.0f0, false)

julia> set_zero_subnormals(false); (x - y, x == y)
(1.0000001f-38, false)

Bazı uygulamalarda, alt normal sayıları sıfırlamanın bir alternatifi, küçük bir gürültü enjekte etmektir. Örneğin, a'yı sıfırlarla başlatmak yerine, onu şu şekilde başlatın:

a = rand(Float32,1000) * 1.f-9

@code_warntype

Makro @code_warntype (veya işlev varyantı code_warntype) bazen türle ilgili sorunları teşhis etmede yardımcı olabilir. İşte bir örnek:

julia> @noinline pos(x) = x < 0 ? 0 : x;

julia> function f(x)
           y = pos(x)
           return sin(y*x + 1)
       end;

julia> @code_warntype f(3.2)
MethodInstance for f(::Float64)
  from f(x) @ Main REPL[9]:1
Arguments
  #self#::Core.Const(f)
  x::Float64
Locals
  y::Union{Float64, Int64}
Body::Float64
1 ─      (y = Main.pos(x))
│   %2 = (y * x)::Float64
│   %3 = (%2 + 1)::Float64
│   %4 = Main.sin(%3)::Float64
└──      return %4

Interpreting the output of @code_warntype, like that of its cousins @code_lowered, @code_typed, @code_llvm, and @code_native, takes a little practice. Your code is being presented in form that has been heavily digested on its way to generating compiled machine code. Most of the expressions are annotated by a type, indicated by the ::T (where T might be Float64, for example). The most important characteristic of @code_warntype is that non-concrete types are displayed in red; since this document is written in Markdown, which has no color, in this document, red text is denoted by uppercase.

En üstte, fonksiyonun çıkarılan dönüş türü Body::Float64 olarak gösterilmektedir. Sonraki satırlar, f'nin Julia'nın SSA IR formundaki gövdesini temsil etmektedir. Numara verilmiş kutular etiketlerdir ve kodunuzdaki atlamalar ( goto aracılığıyla) için hedefleri temsil eder. Gövdeye baktığınızda, ilk olarak pos çağrıldığını ve dönüş değerinin büyük harfle gösterilen Union türü Union{Float64, Int64} olarak çıkarıldığını görebilirsiniz, çünkü bu bir somut olmayan türdür. Bu, pos'un dönüş türünü girdi türlerine dayanarak kesin olarak bilemeyeceğimiz anlamına gelir. Ancak, y*x sonucunun, y bir Float64 veya Int64 olsa da her durumda bir Float64 olduğu gerçeği vardır. Sonuç olarak, f(x::Float64) çıktısında tür açısından dengesiz olmayacaktır, bazı ara hesaplamalar tür açısından dengesiz olsa bile.

Bu bilgiyi nasıl kullanacağınız size kalmış. Açıkça, pos'u tür açısından kararlı hale getirmek en iyi seçenek olacaktır: eğer bunu yaparsanız, f içindeki tüm değişkenler somut hale gelir ve performansı optimal olur. Ancak, bu tür geçici tür kararsızlığının çok da önemli olmadığı durumlar vardır: örneğin, pos hiçbir zaman yalnız kullanılmıyorsa, f'nin çıktısının tür açısından kararlı olması (örneğin, Float64 girdileri için) sonraki kodu tür kararsızlığının yayılma etkilerinden koruyacaktır. Bu, tür kararsızlığını düzeltmenin zor veya imkansız olduğu durumlarda özellikle önemlidir. Bu tür durumlarda, yukarıdaki ipuçları (örneğin, tür açıklamaları eklemek ve/veya fonksiyonları bölmek) tür kararsızlığından kaynaklanan "zararı" sınırlamak için en iyi araçlarınızdır. Ayrıca, Julia Base'in bile tür açısından kararsız olan fonksiyonları olduğunu unutmayın. Örneğin, findfirst fonksiyonu bir anahtarın bulunduğu bir dizideki indeksi döndürür veya bulunamazsa nothing döndürür; bu, belirgin bir tür kararsızlığıdır. Önemli olabilecek tür kararsızlıklarını bulmayı kolaylaştırmak için, missing veya nothing içeren Union'lar sarı renkle vurgulanmıştır, kırmızı yerine.

Aşağıdaki örnekler, yaprak olmayan türler içerdiği belirtilen ifadeleri yorumlamanıza yardımcı olabilir:

  • Fonksiyon gövdesi Body::Union{T1,T2}) ile başlıyor.

    • Yorumlama: kararsız dönüş türüne sahip fonksiyon
    • Öneri: dönen değer tür açısından kararlı hale getirin, gerekirse onu anotlayın.
  • invoke Main.g(%%x::Int64)::Union{Float64, Int64}

    • Yorum: tür-istikrarsız bir fonksiyon g çağrısı.
    • Öneri: fonksiyonu düzeltin veya gerekirse dönüş değerini açıklayın
  • Base.getindex(%%x::Array{Any,1}, 1::Int64)::Any

    • Yorumlama: kötü tipli dizilerin elemanlarına erişim
    • Öneri: Daha iyi tanımlanmış türlerle diziler kullanın veya gerekirse bireysel eleman erişimlerinin türünü anotlayın.
  • Base.getfield(%%x, :(:data))::Array{Float64,N} where N

    • Yorum: yaprak olmayan türde bir alan almak. Bu durumda, x'in türü, diyelim ki ArrayContainer, data::Array{T} alanına sahipti. Ancak Array, somut bir tür olabilmesi için boyut N'ye de ihtiyaç duyar.
    • Öneri: Array{T,3} veya Array{T,N} gibi somut türler kullanın; burada N, artık ArrayContainer'ın bir parametresidir.

Performance of captured variable

Aşağıdaki iç fonksiyonu tanımlayan örneği dikkate alın:

function abmult(r::Int)
    if r < 0
        r = -r
    end
    f = x -> x * r
    return f
end

Fonksiyon abmult, r'nin mutlak değeri ile argümanını çarpan bir f fonksiyonu döndürür. f'ye atanan iç fonksiyona "closure" denir. İç fonksiyonlar, dil tarafından do blokları ve jeneratör ifadeleri için de kullanılır.

Bu kod stili, dil için performans zorlukları sunar. Ayrıştırıcı, bunu daha düşük seviyeli talimatlara çevirirken, yukarıdaki kodu önemli ölçüde yeniden düzenleyerek iç işlevi ayrı bir kod bloğuna çıkarır. İç işlevler ve onları saran kapsam tarafından paylaşılan "yakalanmış" değişkenler, r gibi, hem iç hem de dış işlevlere erişilebilen yığın tahsisli bir "kutucuğa" da çıkarılır çünkü dil, iç kapsamda r'nin dış kapsamda r ile aynı olması gerektiğini belirtir; bu, dış kapsam (veya başka bir iç işlev) r'yi değiştirdikten sonra bile geçerlidir.

Önceki paragraftaki tartışma "parsing" ile ilgiliydi, yani abmult içeren modülün ilk yüklendiği sırada gerçekleşen derleme aşaması, ilk kez çağrıldığı sonraki aşamaya karşı. Parser, Int'in sabit bir tür olduğunu veya r = -r ifadesinin bir Int'i başka bir Int'ye dönüştürdüğünü "bilmez". Tür çıkarımının sihri, derlemenin sonraki aşamasında gerçekleşir.

Böylece, ayrıştırıcı r'nin sabit bir türü (Int) olduğunu bilmez. Ayrıca, iç fonksiyon oluşturulduktan sonra r'nin değerinin değişmeyeceğini de bilmez (bu nedenle kutuya ihtiyaç yoktur). Bu nedenle, ayrıştırıcı, r'nin her bir kullanımında çalışma zamanı türü dağıtımı gerektiren Any gibi soyut bir türü tutan bir kutu için kod üretir. Bu, yukarıdaki fonksiyona @code_warntype uygulayarak doğrulanabilir. Hem kutulama hem de çalışma zamanı türü dağıtımı performans kaybına neden olabilir.

Eğer yakalanan değişkenler kodun performans açısından kritik bir bölümünde kullanılıyorsa, o zaman bu değişkenlerin performansını artırmak için aşağıdaki ipuçları yardımcı olur. İlk olarak, yakalanan bir değişkenin türünün değişmeyeceği biliniyorsa, bu açıkça bir tür açıklaması ile belirtilebilir (değişken üzerinde, sağ taraf üzerinde değil):

function abmult2(r0::Int)
    r::Int = r0
    if r < 0
        r = -r
    end
    f = x -> x * r
    return f
end

Tip anotasyonu, kutudaki nesneye somut bir tür ilişkilendirebildiği için yakalanma nedeniyle kaybolan performansı kısmen geri kazandırır. Daha da ileri giderek, eğer yakalanan değişkenin (kapanış oluşturulduktan sonra yeniden atanmayacağı için) hiç kutulanmasına gerek yoksa, bu let blokları ile aşağıdaki gibi belirtilebilir.

function abmult3(r::Int)
    if r < 0
        r = -r
    end
    f = let r = r
            x -> x * r
    end
    return f
end

let bloğu, yalnızca iç işlevin kapsamına sahip yeni bir r değişkeni oluşturur. İkinci teknik, yakalanmış değişkenlerin varlığında tam dil performansını geri kazandırır. Bunun, derleyicinin hızla gelişen bir yönü olduğunu ve gelecekteki sürümlerin bu düzeyde programcı notasyonu gerektirmeyeceğini belirtmek önemlidir. Bu arada, FastClosures gibi bazı kullanıcı katkılı paketler, abmult3'te olduğu gibi let ifadelerinin eklenmesini otomatikleştirir.

Multithreading and linear algebra

Bu bölüm, her bir iş parçacığında lineer cebir işlemleri gerçekleştiren çok iş parçacıklı Julia koduna uygulanır. Gerçekten de, bu lineer cebir işlemleri, kendileri de çok iş parçacıklı olan BLAS / LAPACK çağrılarını içerir. Bu durumda, iki farklı çok iş parçacıklı tür nedeniyle çekirdeklerin aşırı abone olmamasını sağlamak gerekir.

Julia, lineer cebir için kendi OpenBLAS kopyasını derler ve kullanır; bu kopyanın iş parçacığı sayısı OPENBLAS_NUM_THREADS ortam değişkeni ile kontrol edilir. Bu, Julia'yı başlatırken bir komut satırı seçeneği olarak ayarlanabilir veya Julia oturumu sırasında BLAS.set_num_threads(N) ile değiştirilebilir (alt modül BLAS, using LinearAlgebra ile dışa aktarılır). Mevcut değeri BLAS.get_num_threads() ile erişilebilir.

Kullanıcı hiçbir şey belirtmediğinde, Julia, OpenBLAS iş parçacıklarının sayısı için makul bir değer seçmeye çalışır (örneğin, platforma, Julia sürümüne vb. dayanarak). Ancak, değeri manuel olarak kontrol etmek ve ayarlamak genellikle önerilir. OpenBLAS davranışı şu şekildedir:

  • Eğer OPENBLAS_NUM_THREADS=1 ise, OpenBLAS çağıran Julia iş parçacığını(larını) kullanır, yani hesaplamayı yürüten Julia iş parçacığında "yaşar".
  • Eğer OPENBLAS_NUM_THREADS=N>1 ise, OpenBLAS kendi thread havuzunu oluşturur ve yönetir (N toplamda). Tüm Julia thread'leri arasında paylaşılan sadece bir OpenBLAS thread havuzu vardır.

Julia'yı çok iş parçacıklı modda JULIA_NUM_THREADS=X ile başlattığınızda, genellikle OPENBLAS_NUM_THREADS=1 olarak ayarlamanız önerilir. Yukarıda açıklanan davranış göz önüne alındığında, BLAS iş parçacıklarının sayısını N>1 olarak artırmak, özellikle N<<X olduğunda, çok kolay bir şekilde daha kötü performansa yol açabilir. Ancak bu sadece bir kuraldır ve her bir iş parçacığı sayısını ayarlamanın en iyi yolu, belirli uygulamanızda denemeler yapmaktır.

Alternative linear algebra backends

OpenBLAS'a alternatif olarak, lineer cebir performansına yardımcı olabilecek birkaç başka arka uç bulunmaktadır. Öne çıkan örnekler arasında MKL.jl ve AppleAccelerate.jl bulunmaktadır.

Bunlar harici paketlerdir, bu nedenle burada ayrıntılı olarak tartışmayacağız. Lütfen ilgili belgelerine başvurun (özellikle çok iş parçacıklı çalışma ile ilgili olarak OpenBLAS'tan farklı davranışları olduğu için).

Execution latency, package loading and package precompiling time

Reducing time to first plot etc.

İlk kez bir julia yöntemi çağrıldığında, bu yöntem (ve çağırdığı herhangi bir yöntem veya statik olarak belirlenebilenler) derlenecektir. @time makro ailesi bunu göstermektedir.

julia> foo() = rand(2,2) * rand(2,2)
foo (generic function with 1 method)

julia> @time @eval foo();
  0.252395 seconds (1.12 M allocations: 56.178 MiB, 2.93% gc time, 98.12% compilation time)

julia> @time @eval foo();
  0.000156 seconds (63 allocations: 2.453 KiB)

@time @eval derlemenin süresini ölçmek için daha iyidir çünkü @eval olmadan, bazı derlemeler zamanlamanın başlamasından önce yapılmış olabilir.

Bir paket geliştirirken, kullanıcılarınızın deneyimini ön derleme ile iyileştirebilirsiniz, böylece paketlerini kullandıklarında kullandıkları kod zaten derlenmiş olur. Paket kodunu etkili bir şekilde ön derlemek için, tipik paket kullanımını temsil eden bir "ön derleme iş yükü" çalıştırmak üzere PrecompileTools.jl kullanmanız önerilir. Bu, yerel derlenmiş kodu paket pkgimage önbelleğine kaydedecek ve bu tür kullanım için "ilk yürütme süresini" (genellikle TTFX olarak adlandırılır) büyük ölçüde azaltacaktır.

Not edin ki PrecompileTools.jl iş yükleri devre dışı bırakılabilir ve bazen önceden derleme yapmak istemiyorsanız Tercihler aracılığıyla yapılandırılabilir; bu, bir paketin geliştirilmesi sırasında geçerli olabilir.

Reducing package loading time

Paketin yüklenme süresini düşük tutmak genellikle faydalıdır. Paket geliştiricileri için genel iyi uygulamalar şunları içerir:

  1. Bağlılıklarınızı gerçekten ihtiyaç duyduğunuzlarla sınırlayın. Temel bağımlılıklarınızı şişirmeden diğer paketlerle birlikte çalışabilirliği desteklemek için package extensions kullanmayı düşünün.
  2. __init__() fonksiyonlarının alternatif yoksa kullanılmasından kaçının, özellikle çok fazla derleme tetikleyebilecek veya uzun süre çalışması gerekenler.
  3. Mümkünse, bağımlılıklarınızdan ve paket kodunuzdan invalidations'yi düzeltin.

@time_imports aracı, yukarıdaki faktörleri gözden geçirmek için REPL'de faydalı olabilir.

julia> @time @time_imports using Plots
      0.5 ms  Printf
     16.4 ms  Dates
      0.7 ms  Statistics
               ┌ 23.8 ms SuiteSparse_jll.__init__() 86.11% compilation time (100% recompilation)
     90.1 ms  SuiteSparse_jll 91.57% compilation time (82% recompilation)
      0.9 ms  Serialization
               ┌ 39.8 ms SparseArrays.CHOLMOD.__init__() 99.47% compilation time (100% recompilation)
    166.9 ms  SparseArrays 23.74% compilation time (100% recompilation)
      0.4 ms  Statistics → SparseArraysExt
      0.5 ms  TOML
      8.0 ms  Preferences
      0.3 ms  PrecompileTools
      0.2 ms  Reexport
... many deps omitted for example ...
      1.4 ms  Tar
               ┌ 73.8 ms p7zip_jll.__init__() 99.93% compilation time (100% recompilation)
     79.4 ms  p7zip_jll 92.91% compilation time (100% recompilation)
               ┌ 27.7 ms GR.GRPreferences.__init__() 99.77% compilation time (100% recompilation)
     43.0 ms  GR 64.26% compilation time (100% recompilation)
               ┌ 2.1 ms Plots.__init__() 91.80% compilation time (100% recompilation)
    300.9 ms  Plots 0.65% compilation time (100% recompilation)
  1.795602 seconds (3.33 M allocations: 190.153 MiB, 7.91% gc time, 39.45% compilation time: 97% of which was recompilation)

Bu örnekte birden fazla paket yüklendiğine dikkat edin, bazıları __init__() fonksiyonlarına sahip, bazıları ise derleme yapar, bunların bir kısmı yeniden derleme yapar. Yeniden derleme, önceki paketlerin yöntemleri geçersiz kılmasıyla oluşur; bu durumlarda, takip eden paketler __init__() fonksiyonlarını çalıştırdıklarında, kod çalıştırılmadan önce bazıları yeniden derleme ile karşılaşır.

Ayrıca, SparseArrays bağımlılık ağacında bulunduğu için Statistics uzantısı SparseArraysExt etkinleştirilmiştir. Yani, 0.4 ms Statistics → SparseArraysExt kısmına bakın.

Bu rapor, bağımlılık yükleme süresinin maliyetinin getirdiği işlevselliğe değip değmediğini gözden geçirmek için iyi bir fırsat sunuyor. Ayrıca Pkg yardımcı programı why, dolaylı bir bağımlılığın neden var olduğunu raporlamak için kullanılabilir.

(CustomPackage) pkg> why FFMPEG_jll
  Plots → FFMPEG → FFMPEG_jll
  Plots → GR → GR_jll → FFMPEG_jll

veya bir paketin getirdiği dolaylı bağımlılıkları görmek için, paketi pkg> rm ile kaldırabilir, manifestodan kaldırılan bağımlılıkları görebilir ve ardından değişikliği pkg> undo ile geri alabilirsiniz.

Eğer yükleme süresi, derleme içeren yavaş __init__() yöntemleri tarafından belirleniyorsa, derlenenlerin ne olduğunu belirlemenin bir yolu, --trace-compile=stderr julia argümanını kullanmaktır. Bu, her derlendiğinde precompile ifadesini raporlayacaktır. Örneğin, tam kurulum şöyle olacaktır:

$ julia --startup-file=no --trace-compile=stderr
julia> @time @time_imports using CustomPackage
...

--startup-file=no ifadesine dikkat edin, bu, testi startup.jl dosyanızda bulunan paketlerden izole etmeye yardımcı olur.

Daha fazla yeniden derleme nedenlerinin analizi, SnoopCompile paketini kullanarak gerçekleştirilebilir.