Döngüler neden yinelemeden daha hızlı?


18

Uygulamada, herhangi bir özyinelemenin bir döngü olarak yazılabileceğini (ve tersi (?)) Anlıyorum ve gerçek bilgisayarlarla ölçersek, döngülerin aynı sorun için özyinelemeden daha hızlı olduğunu buluyoruz. Fakat bu farkı yaratan herhangi bir teori var mı yoksa esas olarak emprik mi?


9
Görünüşler, onları kötü uygulayan dillerde yinelemeden daha hızlıdır. Doğru Kuyruk Özyinelemeli bir dilde, özyinelemeli programlar sahnelerin arkasındaki döngülere çevrilebilir, bu durumda özdeş oldukları için hiçbir fark olmaz.
jmite

3
Evet ve bunu destekleyen bir dil kullanıyorsanız, herhangi bir olumsuz performans etkisi olmadan (kuyruk) özyinelemesini kullanabilirsiniz.
jmite

1
@jmite, aslında bir döngüye optimize edilebilir kuyruk özyineleme , son derece nadirdir, düşündüğünüzden çok daha nadirdir. Özellikle referans sayılan değişkenler gibi türleri yöneten dillerde.
Johan - Monica'yı

1
Etiket zaman karmaşıklığını eklediğiniz için, bir döngüye sahip bir algoritmanın özyineleme ile bir algoritma ile aynı zaman karmaşıklığına sahip olduğunu eklemeliyim, ancak ikincisiyle, alınan zaman, özyineleme için ek yük miktarı.
Lieuwe Vinkhuijzen

2
Hey, neredeyse tüm olasılıkları tüketen çok sayıda iyi cevapla ödül eklediğiniz için, ihtiyacınız olan ya da bir şeyin netleştirilmesi gerektiği gibi hissettiğiniz bir şey var mı? Ekleyecek çok şeyim yok, bazı cevapları düzenleyebilir veya yorum bırakabilirim, bu yüzden bu genel (kişisel değil) bir soru.
Kötülük

Yanıtlar:


17

Döngülerin özyinelemeden daha hızlı olmasının nedeni kolaydır.
Bir döngü montajda buna benzer.

mov loopcounter,i
dowork:/do work
dec loopcounter
jmp_if_not_zero dowork

Tek bir koşullu atlama ve döngü sayacı için bazı defter tutma.

Özyineleme (derleyici tarafından optimize edilmediğinde veya optimize edilemediğinde) şöyle görünür:

start_subroutine:
pop parameter1
pop parameter2
dowork://dowork
test something
jmp_if_true done
push parameter1
push parameter2
call start_subroutine
done:ret

Çok daha karmaşık ve en az 3 atlama (1 olup olmadığını görmek için 1 test, bir çağrı ve bir dönüş) olsun.
Ayrıca özyinelemede parametrelerin ayarlanması ve getirilmesi gerekir.
Tüm parametreler önceden ayarlandığından, bu öğelerin hiçbirine döngüde gerek yoktur.

Teorik olarak parametreler özyineleme ile de yerinde kalabilir, ancak bildiğim hiçbir derleyici aslında optimizasyonlarında bu kadar ileri gitmez.

Bir çağrı ve bir jmp arasındaki farklar
Bir çağrı-dönüş çifti, jmp'den çok daha pahalı değildir. Çift 2 döngü alır ve jmp 1 alır; neredeyse hiç fark edilmez.
Kayıt parametrelerini destekleyen çağrı kurallarında , CPU'nun tamponları taşmadığı sürece parametreler için ek yük, ancak yığın parametreleri bile ucuzdur .
Yinelenmeyi yavaşlatan kullanımda arama kuralı ve parametre işleme tarafından belirlenen çağrı kurulumunun genel gideridir.
Bu, uygulamaya çok bağlıdır.

Kötü özyineleme işleme örneği Örneğin, referans olarak sayılan bir parametre geçirilirse (örn. Sabit yönetilmeyen bir tür parametresi), referans sayımının kilitli bir ayarını yaparak 100 döngüyü ekler ve performansı bir döngüye karşı tamamen öldürür.
Özyinelemeye ayarlanmış dillerde bu kötü davranış oluşmaz.

CPU optimizasyonu
Özyinelemenin daha yavaş olmasının bir diğer nedeni de CPU'lardaki optimizasyon mekanizmalarına karşı çalışmasıdır.
İadeler ancak arka arkaya çok fazla değilse doğru tahmin edilebilir. CPU, birkaç avuç giriş içeren bir dönüş yığını arabelleğine sahiptir. Bunlar bittiğinde, her ek getiri yanlış bir şekilde tahmin edilir ve büyük gecikmelere neden olur.
Arabellek boyutunu aşan bir yığın dönüş arabelleği çağrısı tabanlı özyineleme kullanan herhangi bir CPU'da en iyi şekilde kaçınılır.

Özyineleme kullanarak önemsiz kod örnekleri hakkında
Fibonacci sayı üretimi gibi önemsiz bir özyineleme örneği kullanırsanız , bu etkiler oluşmaz, çünkü özyineleme hakkında 'bilen' derleyiciler, tıpkı tuzuna değer herhangi bir programcı gibi bir döngüye dönüşür olacaktır.
Bu önemsiz örnekleri doğru şekilde optimize etmeyen bir ortamda çağırırsanız, çağrı yığını (gereksiz yere) sınırların dışına çıkacaktır.

Kuyruk özyineleme hakkında
Bazen derleyicinin kuyruk özyinelemesini döngü haline getirerek optimize ettiğini unutmayın. Bu davranışa sadece bu konuda iyi bilinen bir dilde sahip olan dillerde güvenmek en iyisidir.
Birçok dil, son geri dönüşten önce kuyruk yinelemesinin optimizasyonunu önleyerek gizli temizleme kodunu ekler.

Doğru ve sözde özyineleme arasındaki karışıklık
Programlama ortamınız özyinelemeli kaynak kodunuzu bir döngüye dönüştürürse, yürütülmekte olan gerçek özyineleme değildir.
Gerçek özyineleme, bir ekmek kırıntıları deposu gerektirir, böylece özyinelemeli rutin çıktıktan sonra adımlarını izleyebilir.
Özyinelemeyi döngü kullanmaktan daha yavaş yapan bu yolun işlenmesidir. Bu etki, yukarıda açıklandığı gibi mevcut CPU uygulamaları ile büyütülür.

Programlama ortamının etkisi
Diliniz özyineleme optimizasyonuna ayarlanmışsa, elbette devam edin ve her fırsatta özyineleme kullanın. Çoğu durumda, dil özyinelemenizi bir tür döngüye dönüştürecektir.
Yapamayacağı durumlarda, programcıya da sert baskı uygulanır. Programlama diliniz özyinelemeye ayarlanmamışsa, etki alanı özyinelemeye uygun değilse bundan kaçınılmalıdır.
Ne yazık ki birçok dil özyinelemeyi iyi işlemez.

Özyinelemenin
yanlış kullanımı Özyineleme kullanarak Fibonacci sekansını hesaplamaya gerek yoktur, aslında patolojik bir örnektir.
Özyineleme en iyi şekilde onu açıkça destekleyen dillerde veya özyinelemenin parladığı alanlarda, örneğin bir ağaçta depolanan verilerin işlenmesi gibi kullanılır.

Herhangi bir özyinelemenin döngü olarak yazılabileceğini anlıyorum

Evet, eğer attan önce arabayı koymak istiyorsan.
Tüm özyineleme örnekleri bir döngü olarak yazılabilir, bu örneklerin bazıları depolama gibi açık bir yığın kullanmanızı gerektirir.
Özyinelemeli kodu bir döngüye dönüştürmek için kendi yığını yuvarlamanız gerekiyorsa, düz özyineleme de kullanabilirsiniz.
Tabii ki bir ağaç yapısında sayıcılar kullanmak gibi özel ihtiyaçlarınız yoksa ve doğru dil desteğiniz yoksa.


16

Bu diğer cevaplar biraz yanıltıcı. Bu eşitsizliği açıklayabilecek uygulama ayrıntılarını belirttiklerini kabul ediyorum, ancak vakayı abartıyorlar. Jmite tarafından doğru bir şekilde önerildiği gibi , işlev çağrıları / özyineleme uygulamalarının kırılması için uygulamaya yöneliktirler . Birçok dil döngüleri özyineleme yoluyla uygular, bu nedenle döngüler bu dillerde açıkça daha hızlı olmayacaktır. Özyineleme hiçbir şekilde teoride (her ikisi de uygulanabilir olduğunda) döngüden daha az verimli değildir. Özeti Guy Steele'in 1977 tarihli "Pahalı Prosedür Çağrısı" Efsanesine ya da Zararlı Sayılan Prosedür Uygulamalarına ya da Lambda: Ultimate GOTO'ya Debunking

Folklor, GOTO ifadelerinin "ucuz" olduğunu, prosedür çağrılarının "pahalı" olduğunu belirtir. Bu efsane büyük ölçüde kötü tasarlanmış dil uygulamalarının bir sonucudur. Bu mitin tarihsel büyümesi düşünülür. Bu efsaneyi çürüten hem teorik fikirler hem de mevcut bir uygulama tartışılmıştır. Prosedür çağrılarının sınırsız kullanımının büyük üslup özgürlüğüne izin verdiği gösterilmiştir. Özellikle, herhangi bir akış şeması, ekstra değişkenler tanıtılmadan "yapılandırılmış" bir program olarak yazılabilir. GOTO ifadesi ve prosedür çağrısı ile ilgili zorluk soyut programlama kavramları ve somut dil yapıları arasında bir çatışma olarak nitelendirilir.

"Soyut programlama kavramları ve beton dil yapıları arasında çatışma" çoğu teorik modeller, örneğin, türsüz olmasından görülebilir lambda hesabı , bir yığın yok . Tabii ki, yukarıdaki makalenin gösterdiği ve Haskell gibi özyineleme dışında yineleme mekanizması olmayan diller tarafından da gösterildiği gibi, bu çatışma gerekli değildir.

fixfix f x = f (fix f) x(λx.M)N-M[N-/x][N-/x]xMN-

Şimdi bir örnek verelim. Define factolarak

fact = fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1

İşte fact 3burada, kompaktlık giçin eşanlamlı olarak kullanacağım fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)), yani fact = g 1. Bu benim argümanımı etkilemez.

fact 3 
~> g 1 3
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1 3 
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 1 3
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 1 3
~> (λn.if n == 0 then 1 else g (1*n) (n-1)) 3
~> if 3 == 0 then 1 else g (1*3) (3-1)
~> g (1*3) (3-1)
~> g 3 2
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 3 2
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 3 2
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 3 2
~> (λn.if n == 0 then 3 else g (3*n) (n-1)) 2
~> if 2 == 0 then 3 else g (3*2) (2-1)
~> g (3*2) (2-1)
~> g 6 1
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 1
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 1
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 1
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 1
~> if 1 == 0 then 6 else g (6*1) (1-1)
~> g (6*1) (1-1)
~> g 6 0
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 0
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 0
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 0
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 0
~> if 0 == 0 then 6 else g (6*0) (0-1)
~> 6

Büyümenin olmadığı ve her yinelemenin aynı miktarda alana ihtiyaç duyduğu ayrıntılarına bile bakmadan şekilden görebilirsiniz. (Teknik olarak, kaçınılmaz olan ve bir whiledöngü için de geçerli olan sayısal sonuç büyür .) Burada sınırsızca büyüyen "yığını" işaret etmek için meydan okuyorum.

Görünüşe göre lambda hesabının arketipik semantiği, yaygın olarak "kuyruk çağrısı optimizasyonu" olarak adlandırılan şeyi zaten yapıyor. Tabii ki burada hiçbir "optimizasyon" gerçekleşmiyor. Burada "normal" çağrıların aksine "kuyruk" çağrıları için özel bir kural yoktur. Bu nedenle, işlev çağrısı semantiğinin birçok soyut karakterizasyonunda olduğu gibi, kuyruk çağrısı "optimizasyonu" nun ne yaptığının "soyut" bir karakterizasyonunu vermek zordur, kuyruk çağrısı "optimizasyonu" için hiçbir şey yapılmaz!

factBirçok dilde "yığın taşması" nın benzer tanımının, bu diller tarafından işlev çağrısı semantiği doğru bir şekilde uygulanamamasıdır. (Bazı dillerin bir mazereti vardır.) Durum, bağlantılı listelerle diziler uygulayan bir dil uygulamasına kabaca benzemektedir. Bu tür "dizilere" endeksleme, dizilerin beklentisini karşılamayan bir O (n) işlemi olacaktır. Bağlantılı listeler yerine gerçek diziler kullanılan dilin ayrı bir uygulamasını yapmış olsaydım, "dizi erişim optimizasyonu" uyguladığımı söyleyemezsiniz, dizilerin bozuk bir uygulamasını düzelttiğimi söylerdiniz.

Yani Veedrac'ın cevabına cevap veriyor. Yığınlar özyineleme için "temel" değildir . Değerlendirme sırasında "yığın benzeri" davranış oluştukça, bu sadece döngülerin (yardımcı veri yapısı olmadan) ilk etapta uygulanamayacağı durumlarda olabilir! Başka bir deyişle, tam olarak aynı performans özelliklerine sahip özyineleme ile döngüler uygulayabilirim. Aslında, Şema ve SML'nin her ikisi de döngü yapıları içerir, ancak her ikisi de bunları özyineleme açısından tanımlar (ve en azından Şema'da, dogenellikle özyinelemeli çağrılara genişleyen bir makro olarak uygulanır .) Benzer şekilde Johan'ın cevabı için hiçbir şey derleyici Johan özyineleme için açıklanan derleme yaymak zorundadır. Aslında,döngüler veya özyineleme kullansanız da tam olarak aynı derleme. Derleyicinin, Johan'ın açıkladığı gibi, bir şekilde bir şekilde yaymak zorunda kaldığı tek zaman, yine de bir döngü tarafından ifade edilemeyen bir şey yaptığınız zamandır . Steele'nin makalesinde belirtildiği ve Haskell, Scheme ve SML gibi gerçek dillerin pratiği ile gösterildiği gibi, kuyruk çağrılarının "optimize edilebildiği" "son derece nadir" değildir, her zamanoptimize edilmelidir. Özyinelemenin belirli bir kullanımının sabit alanda çalışıp çalışmayacağı, nasıl yazıldığına bağlıdır, ancak bunu mümkün kılmak için uygulamanız gereken kısıtlamalar, sorununuzu bir döngü şekline sığdırmak için ihtiyaç duyacağınız kısıtlamalardır. (Aslında, daha az sıkıdırlar. Yardımcı makineleri gerektiren döngülerin aksine kuyruk çağrıları yoluyla daha temiz ve verimli bir şekilde işlenen durum makinelerini kodlama gibi sorunlar vardır.) Yine, tekrarlamanın daha fazla iş yapmasını gerektiren tek zaman kodunuz zaten bir döngü olmadığında.

Benim tahminim Johan kuyruk çağrısı "optimizasyonu" ne zaman gerçekleştireceği konusunda keyfi kısıtlamaları olan C derleyicilerinden söz ediyor. Johan ayrıca "yönetilen türlere sahip diller" hakkında konuşurken C ++ ve Rust gibi dillerden bahsediyor. C ++ 'dan gelen ve Rust'ta bulunan RAII deyimi, yüzeysel olarak kuyruk çağrılarına değil, kuyruk çağrılarına benzeyen şeyler yapar (çünkü "yıkıcılar" hala çağrılmalıdır). Kuyruk özyinelemesine izin verecek biraz farklı bir semantiğe kaydolmak için farklı bir sözdizimi kullanma önerileri olmuştur (yani daha önce yıkıcıları çağırınız)son kuyruk çağrısı ve "yıkılan" nesnelere erişime açıkça izin verilmiyor). (Çöp toplamanın böyle bir sorunu yoktur ve tüm Haskell, SML ve Şema çöp toplanan dillerdir.) Oldukça farklı bir şekilde, Smalltalk gibi bazı diller, birinci sınıf bir nesne olarak "yığını" ortaya koyarlar. "yığın" artık bir uygulama ayrıntısı değildir, ancak bu farklı semantiklerle ayrı arama türlerine sahip olmayı engellemez. (Java, güvenliğin bazı yönlerini ele alma biçimi nedeniyle yapılamayacağını söylüyor, ancak bu aslında yanlış .)

Uygulamada, işlev çağrılarının kırık uygulamalarının yaygınlığı üç ana faktörden gelir. İlk olarak, birçok dil, bozuk uygulamayı uygulama dilinden (genellikle C) devralır. İkincisi, deterministik kaynak yönetimi güzel ve konuyu daha karmaşık hale getiriyor, ancak sadece birkaç dil bunu sunuyor. Üçüncüsü ve benim tecrübelerime göre, çoğu insanın önem vermesinin nedeni, hata ayıklama amacıyla hatalar oluştuğunda yığın izleri istemeleridir. Sadece ikinci neden, potansiyel olarak teorik olarak motive edilebilen nedendir.


Ben mantıksal olarak bu şekilde olması gerekip gerekmediğine değil (iki programın muhtemelen aynı olması nedeniyle) iddianın doğru olmasının en temel nedenine atıfta bulunmak için "temel" i kullandım. Ama bir bütün olarak yorumunuza katılmıyorum. Lambda taşı kullanımınız, yığını belirsiz olduğu kadar kaldırmaz.
Veedrac

İddia "Derleyicinin Johan'ın tarif ettiği gibi montaj yapmakla (bir şekilde) mecbur kaldığı tek zaman, yine de bir döngü tarafından ifade edilemeyen bir şey yaptığınız zamandır." aynı zamanda oldukça garip; bir derleyici (normalde) aynı çıktıyı üreten herhangi bir kodu üretebilir, bu nedenle yorumunuz temel olarak bir totolojidir. Ancak pratikte derleyiciler farklı eşdeğer programlar için farklı kodlar üretiyorlardı ve soru bunun sebebi hakkındaydı.
Veedrac

Ö(1)

Bir benzetme yapmak için, döngülere değişmez dizelerin eklenmesinin neden "olması gerekmiyor" ile kuadratik zaman aldığı sorusuna cevap vermek tamamen makul olacaktır, ancak uygulamanın bu şekilde bozulduğunu iddia etmek devam etmeyecektir.
Veedrac

Çok ilginç bir cevap. Rağmen biraz bir rant gibi geliyor :-). Yeni bir şey öğrendiğim için değerlendirildim.
Johan - Monica'yı

2

Temel olarak fark, özyinelemenin bir yığın, muhtemelen istemediğiniz bir yardımcı veri yapısı içermesidir, oysa döngüler otomatik olarak bunu yapmaz. Sonuçta, yığına gerçekten ihtiyacınız olmadığını belirleyebilen tipik bir derleyici sadece nadir durumlarda.

Bunun yerine, ayrılmış bir yığın üzerinde el ile çalışan döngüleri karşılaştırırsanız (örn. Yığın bellek için bir işaretçi aracılığıyla), bunları normalde donanım yığınını kullanmaktan daha hızlı veya daha yavaş bulamazsınız.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.