Kuyruk özyineleme nedir?


1694

Lisp öğrenmeye başlarken kuyruk özyinelemeli terimiyle karşılaştım . Tam olarak ne anlama geliyor?


154
Merak etmek için: hem uzun süre hem de dilde. Eski İngilizce kullanımdayken; Orta İngilizce ise süre gelişimidir. Bağlaç olarak anlam olarak değiştirilebilirler, ancak standart Amerikan İngilizcesinde hayatta kalmamışlardır.
Filip Bartuzi

14
Belki geç, ama bu kuyruk özyinelemeyle ilgili oldukça iyi bir makale: programmerinterview.com/index.php/recursion/tail-recursion
Sam003

5
Kuyruk özyinelemeli bir işlevi tanımlamanın en büyük faydalarından biri, yinelemeli bir forma dönüştürülebilmesi ve böylece algoritmayı yöntem-yığın-tepegözünden yeniden serbest bırakabilmesidir. @Kyle Cronin ve diğer birkaç kişinin yanıtını ziyaret etmek hoşunuza gidebilir
KGhatak

@Yesudeep'ten bu bağlantı bulduğum en iyi, en ayrıntılı açıklama - lua.org/pil/6.3.html
Jeff Fischer

1
Birisi bana söyleyebilir misiniz, Birleştirme sıralaması ve hızlı sıralama kuyruğu özyineleme (TRO) kullanıyor mu?
majurageerthan

Yanıtlar:


1719

İlk N doğal sayıyı toplayan basit bir işlevi düşünün. (örneğin sum(5) = 1 + 2 + 3 + 4 + 5 = 15).

İşte özyineleme kullanan basit bir JavaScript uygulaması:

function recsum(x) {
    if (x === 1) {
        return x;
    } else {
        return x + recsum(x - 1);
    }
}

Aradıysanız recsum(5), JavaScript yorumlayıcı bunu değerlendirir:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15

JavaScript yorumlayıcısı toplamı hesaplama işini yapmaya başlamadan önce her yinelemeli çağrının nasıl tamamlanması gerektiğini unutmayın.

İşte aynı işlevin kuyruk özyinelemeli sürümü:

function tailrecsum(x, running_total = 0) {
    if (x === 0) {
        return running_total;
    } else {
        return tailrecsum(x - 1, running_total + x);
    }
}

İşte tailrecsum(5)( tailrecsum(5, 0)varsayılan ikinci argüman nedeniyle etkili olur) çağırırsanız gerçekleşecek olaylar dizisi .

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

Kuyruk özyinelemeli durumda, özyinelemeli çağrının her değerlendirmesi running_totalile güncellenir.

Not: Orijinal cevap Python'dan örnekler kullanmıştır. Bunlar JavaScript olarak değiştirildi, çünkü Python yorumlayıcıları kuyruk çağrısı optimizasyonunu desteklemiyor . Ancak, kuyruk çağrısı optimizasyonu ECMAScript 2015 spesifikasyonunun bir parçası olsa da , çoğu JavaScript tercümanı bunu desteklemez .


32
Kuyruk özyineleme ile son cevabın sadece yöntemin SON çağrılmasıyla hesaplandığını söyleyebilir miyim? Kuyruk özyineleme DEĞİLSE, cevabı hesaplamak için tüm yöntem için tüm sonuçlara ihtiyacınız vardır.
chrisapotek

2
İşte Lua'da birkaç örnek sunan bir zeyilname: lua.org/pil/6.3.html Bunu yapmak da faydalı olabilir! :)
yesudeep

2
Birisi lütfen chrisapotek'in sorusunu ele alabilir mi? tail recursionKuyruk çağrılarını optimize etmeyen bir dilde nasıl başarılabileceğime kafam karıştı .
Kevin Meredith

3
@KevinMeredith "kuyruk özyineleme", bir fonksiyondaki son ifadenin, aynı fonksiyona özyinelemeli çağrı olduğu anlamına gelir. Bunu, bu özyinelemeyi optimize etmeyen bir dilde yapmanın bir anlamı olmadığından eminsiniz. Bununla birlikte, bu cevap kavramı (neredeyse) doğru bir şekilde göstermektedir. "Else:" atlanmış olsaydı, bu daha açık bir şekilde bir kuyruk çağrısı olurdu. Davranışı değiştirmez, ancak kuyruk çağrısını bağımsız bir ifade olarak yerleştirir. Bunu bir düzenleme olarak göndereceğim.
ToolmakerSteve

2
Yani python'da bir avantaj yoktur çünkü tailrecsum işlevine yapılan her çağrıda yeni bir yığın çerçevesi oluşturulur - değil mi?
Quazi Irfan

707

Gelen geleneksel özyineleme , tipik modeli öncelikle tekrarlanan aramalara gerçekleştirmek ve sonra özyinelemeli çağrısının dönüş değeri alıp sonucu hesaplamak olmasıdır. Bu şekilde, her özyinelemeli çağrıdan dönene kadar hesaplamanızın sonucunu alamazsınız.

Gelen kuyruk özyineleme , öncelikle hesaplamalar ve sonra bir sonraki özyinelemeli adıma mevcut adımın sonuçlarını geçen özyinelemeli çağrı yürütün. Bu son ifadenin şeklinde olur (return (recursive-function params)). Temel olarak, herhangi bir özyinelemeli adımın dönüş değeri, bir sonraki özyinelemeli çağrının dönüş değeri ile aynıdır .

Bunun sonucu, bir sonraki özyinelemeli adımı gerçekleştirmeye hazır olduğunuzda, artık geçerli yığın çerçevesine ihtiyacınız kalmamasıdır. Bu, bazı optimizasyonlara izin verir. Aslında, uygun bir şekilde yazılmış bir derleyici ile, asla kuyruk tekrarlayan çağrı ile yığın taşması keskin nişancısı olmamalıdır . Bir sonraki özyinelemeli adım için mevcut yığın çerçevesini yeniden kullanmanız yeterlidir. Eminim Lisp bunu yapar.


17
"Eminim Lisp bunu yapar" - Şema yapar, ama Common Lisp her zaman yapmaz.
Aaron

2
@Daniel "Temelde, herhangi bir özyinelemeli adımın dönüş değeri, bir sonraki özyinelemeli çağrının dönüş değeriyle aynıdır." - Lorin Hochstein tarafından yayınlanan kod snippet'i için bu argümanın doğru olduğunu göremiyorum. Lütfen biraz açıklayabilir misiniz?
Geek

8
@Geek Bu gerçekten geç bir yanıt, ama bu Lorin Hochstein'ın örneğinde doğrudur. Her adım için hesaplama, yinelemeli çağrıdan önce değil, sonra yapılır. Sonuç olarak, her durak değeri doğrudan önceki adımdan döndürür. Son özyinelemeli çağrı hesaplamayı bitirir ve ardından son sonucu değiştirmeden çağrı yığınına geri döndürür.
reirab

3
Scala bunu yapıyor ama uygulamak için @tailrec'e ihtiyacınız var.
SilentDirge

2
"Bu şekilde, her özyinelemeli çağrıdan dönene kadar hesaplamanızın sonucunu alamazsınız." - belki bunu yanlış anladım, ancak bu, özellikle tüm özyinelemeleri çağırmadan sonuç almanın tek yolunun geleneksel özyinelemede olduğu gibi tembel diller için geçerli değildir (örneğin, && ile sonsuz bir Bools listesine katlanmak).
hasufell

206

Önemli bir nokta, kuyruk özyinelemesinin esasen döngüye eşdeğer olmasıdır. Bu sadece derleyici optimizasyonu değil, ifadeyle ilgili temel bir gerçek. Bu iki yöne de gider: formun herhangi bir döngüsünü alabilirsiniz

while(E) { S }; return Q

Burada Eve Qifadelerdir ve Sbir ifade dizisidir ve onu kuyruk yinelemeli işleve dönüştürür

f() = if E then { S; return f() } else { return Q }

Tabii ki, E, Sve Qbazı değişkenler üzerinde bazı ilginç değerini hesaplamak için tanımlanmak zorundadır. Örneğin, döngü işlevi

sum(n) {
  int i = 1, k = 0;
  while( i <= n ) {
    k += i;
    ++i;
  }
  return k;
}

kuyruk özyinelemeli işlev (ler) e eşdeğerdir

sum_aux(n,i,k) {
  if( i <= n ) {
    return sum_aux(n,i+1,k+i);
  } else {
    return k;
  }
}

sum(n) {
  return sum_aux(n,1,0);
}

(Kuyruk özyinelemeli fonksiyonun daha az parametreye sahip bir fonksiyonla bu "sarılması" yaygın bir fonksiyonel deyimdir.)


@LorinHochstein'ın cevabında, açıklamasına göre, özyinelemeli kısmın "Dönüş" ü takip ettiği zamanki kuyruk özyinelemesini anladım, ancak sizinkinde, kuyruk özyinelemeli değildir. Örneğinizin düzgün özyineleme olarak kabul edildiğinden emin misiniz?
CodyBugstein

1
@Imray Kuyruk özyinelemeli bölüm sum_aux içindeki "return sum_aux" deyimidir.
Chris Conway

1
@lmray: Chris'in kodu aslında eşdeğerdir. İf / then'in sırası ve sınırlama testinin stili ... eğer x == 0'a karşı (i <= n) ... takılmak için bir şey değildir. Mesele, her yinelemenin sonucunu bir sonrakine geçirmesidir.
Taylor

else { return k; }değiştirilebilirreturn k;
c0der

144

Lua'da Programlama kitabından yapılan bu alıntı, uygun bir kuyruk özyinelemesinin nasıl yapılacağını gösterir (Lua'da, ancak Lisp için de geçerli olmalıdır) ve neden daha iyi olduğunu gösterir.

Bir kuyruk çağrısı [kuyruk özyineleme] bir çağrı olarak giyinmiş bir tür gotodur. Kuyruk çağrısı, bir işlev son eylemi olarak başka bir işlev çağırdığında gerçekleşir; Örneğin, aşağıdaki kodda, çağrı gbir kuyruk çağrısıdır:

function f (x)
  return g(x)
end

fÇağrılardan sonra g, başka bir şey yapmaz. Bu gibi durumlarda, çağrılan işlev sona erdiğinde programın çağrı işlevine dönmesi gerekmez. Bu nedenle, kuyruk çağrısından sonra, programın çağırma işlevi hakkında yığındaki herhangi bir bilgiyi tutması gerekmez. ...

Düzgün bir kuyruk çağrısı yığın alanı kullanmadığından, bir programın yapabileceği "iç içe" kuyruk çağrılarının sayısında bir sınır yoktur. Örneğin, argüman olarak herhangi bir sayı ile aşağıdaki işlevi çağırabiliriz; hiçbir zaman yığını taşmaz:

function foo (n)
  if n > 0 then return foo(n - 1) end
end

... Daha önce söylediğim gibi, bir kuyruk çağrısı bir çeşit goto. Bu nedenle, Lua'da uygun kuyruk çağrılarının oldukça yararlı bir uygulaması, durum makinelerini programlamak içindir. Bu tür uygulamalar her durumu bir fonksiyonla temsil edebilir; durumu değiştirmek, belirli bir işleve gitmek (veya çağırmak) içindir. Örnek olarak, basit bir labirent oyunu ele alalım. Labirent, her biri dört kapıya kadar çeşitli odalara sahiptir: kuzey, güney, doğu ve batı. Her adımda, kullanıcı bir hareket yönüne girer. Bu yönde bir kapı varsa, kullanıcı ilgili odaya gider; aksi takdirde program bir uyarı yazdırır. Amaç başlangıç ​​odasından son odaya gitmek.

Bu oyun, mevcut odanın durum olduğu tipik bir durum makinesidir. Bu labirenti her oda için bir işlevle uygulayabiliriz. Bir odadan diğerine geçmek için kuyruk çağrıları kullanıyoruz. Dört odalı küçük bir labirent şöyle görünebilir:

function room1 ()
  local move = io.read()
  if move == "south" then return room3()
  elseif move == "east" then return room2()
  else print("invalid move")
       return room1()   -- stay in the same room
  end
end

function room2 ()
  local move = io.read()
  if move == "south" then return room4()
  elseif move == "west" then return room1()
  else print("invalid move")
       return room2()
  end
end

function room3 ()
  local move = io.read()
  if move == "north" then return room1()
  elseif move == "east" then return room4()
  else print("invalid move")
       return room3()
  end
end

function room4 ()
  print("congratulations!")
end

Gördüğünüz gibi, özyinelemeli bir çağrı yaptığınızda:

function x(n)
  if n==0 then return 0
  n= n-2
  return x(n) + 1
end

Bu özyineli özyinelemeli değildir, çünkü özyinelemeli çağrı yapıldıktan sonra bu işlevde hala yapılacak işler (1 ekleyin) vardır. Çok yüksek bir sayı girerseniz, büyük olasılıkla yığın taşmasına neden olur.


9
Bu harika bir cevap çünkü kuyruk çağrılarının yığın boyutuna etkilerini açıklıyor.
Andrew Swan

@AndrewSwan Aslında, bu soruya rastlayabilecek orijinal askerin ve ara sıra okuyucunun kabul edilen cevapla daha iyi hizmet edilebileceğine inanmama rağmen (yığının gerçekte ne olduğunu bilmeyebileceğinden). vantilatör.
Hoffmann

1
Benim favori cevap yanı sıra yığın boyutu için ima dahil.
njk2015

80

Düzenli özyineleme kullanarak, her özyinelemeli çağrı, çağrı yığınına başka bir giriş gönderir. Özyineleme tamamlandığında, uygulama daha sonra her girişi tamamen geri almalıdır.

Kuyruk özyineleme ile, dile bağlı olarak derleyici yığını bir girişe daraltabilir, böylece yığın alanından tasarruf edebilirsiniz ... Büyük bir özyinelemeli sorgu aslında bir yığın taşmasına neden olabilir.

Temel olarak Kuyruk özyinelemeleri yinelemeye göre optimize edilebilir.


1
"Büyük bir özyinelemeli sorgu aslında bir yığın taşmasına neden olabilir." 1. paragrafta olmalı, 2. (kuyruk özyineleme) birinde değil mi? Kuyruk özyinelemenin en büyük avantajı, yığındaki çağrıları "biriktirmemek" için (ör: Şema) optimize edilebilmesidir, bu nedenle çoğunlukla yığın taşmalarını önleyecektir!
Olivier Dulac

70

Jargon dosyası, kuyruk özyineleme tanımı hakkında şunları söyleyecektir:

kuyruk özyineleme /n./

Zaten hasta değilseniz, kuyruk yinelemesine bakın.


68

Bunu kelimelerle açıklamak yerine, bir örnek. Bu faktöriyel fonksiyonun bir Şema versiyonudur:

(define (factorial x)
  (if (= x 0) 1
      (* x (factorial (- x 1)))))

İşte kuyruk özyinelemeli faktöriyel bir sürümü:

(define factorial
  (letrec ((fact (lambda (x accum)
                   (if (= x 0) accum
                       (fact (- x 1) (* accum x))))))
    (lambda (x)
      (fact x 1))))

İlk versiyonda, özyinelemeli çağrının çarpma ifadesine beslendiğini ve dolayısıyla özyinelemeli çağrı yaparken durumun yığına kaydedilmesi gerektiğini fark edeceksiniz. Kuyruk özyinelemeli versiyonda, özyinelemeli çağrının değerini bekleyen başka bir S ifadesi yoktur ve yapılacak başka bir iş olmadığından, durumun yığına kaydedilmesi gerekmez. Kural olarak, Şema kuyruk yinelemeli işlevleri sabit yığın alanı kullanır.


4
Kuyruk özyinelemelerinin yinelemeli bir forma dönüştürülebildikleri ve böylece O (1) bellek karmaşıklık formuna dönüştürülebildikleri en önemli yönünü belirtmek için +1.
KGhatak

1
@KGhatak tam olarak değil; cevap genel olarak bellekten değil, "sabit yığın alanı" ndan doğru olarak bahseder. çirkin olmak için değil, sadece yanlış anlama olmadığından emin olmak için. Örneğin kuyruk-özyinelemeli liste-kuyruk-mutasyon list-reverseprosedürü sabit yığın uzayında çalışır, ancak öbek üzerinde bir veri yapısı oluşturur ve büyütür. Bir ağaç geçişi ek bir argümanda simüle edilmiş bir yığın kullanabilir. vb.
Ness

45

Kuyruk özyineleme özyinelemeli çağrının özyinelemeli algoritmada son mantık komutunda son olduğunu belirtir.

Genellikle özyinelemede, özyinelemeli çağrıları durduran ve çağrı yığınını açmaya başlayan bir temel durum vardır. Klasik bir örnek kullanmak için, Lisp'den daha fazla C-ish olsa da, faktöriyel fonksiyon kuyruk yinelemesini göstermektedir. Yinelemeli çağrı , temel durum durumunu kontrol ettikten sonra gerçekleşir .

factorial(x, fac=1) {
  if (x == 1)
     return fac;
   else
     return factorial(x-1, x*fac);
}

Faktöriğe ilk çağrı factorial(n)burada olacaktır fac=1(varsayılan değer) ve n faktöriyelin hesaplanacağı sayıdır.


Açıklamanızı en kolay buldum, ama eğer gidecek bir şey varsa, kuyruk özyineleme sadece bir deyim temel vakaları olan işlevler için yararlıdır. Bu postimg.cc/5Yg3Cdjn gibi bir yöntem düşünün . Not: dış else, "temel durum" olarak adlandırabileceğiniz, ancak birkaç satıra yayılan adımdır. Seni yanlış mı anladım yoksa varsayım doğru mu? Kuyruk özyineleme sadece bir gömlek için iyidir?
Cevapları İstiyorum

2
@IWantAnswers - Hayır, işlevin gövdesi isteğe bağlı olarak büyük olabilir. Kuyruk çağrısı için gerekli olan tek şey, içinde bulunduğu dalın işlevi yaptığı en son şey olarak çağırması ve işlevi çağırmanın sonucunu döndürmesidir. factorialÖrnek hepsi bu, sadece klasik basit bir örnektir.
TJ Crowder

28

Bu, talimat işaretçisini yığının üzerine itmek yerine, özyinelemeli bir işlevin üstüne atlayıp yürütmeye devam edebileceğiniz anlamına gelir. Bu, fonksiyonların yığını taşmadan süresiz olarak geri çekilmesini sağlar.

Konu üzerine, yığın çerçevelerinin neye benzediğine dair grafik örnekleri olan bir blog yazısı yazdım .


21

İşte iki işlevi karşılaştıran bir hızlı kod pasajı. Birincisi, belirli bir sayının faktöriyelini bulmak için geleneksel özyineleme. İkincisi kuyruk özyineleme kullanır.

Anlamak çok basit ve sezgisel.

Yinelemeli bir işlevin kuyruk yinelemeli olup olmadığını anlamanın kolay bir yolu, temel durumda somut bir değer döndürüp döndürmediğidir. Yani 1 veya doğru ya da bunun gibi bir şey döndürmez. Büyük olasılıkla yöntem parametrelerinden birinin bazı varyantlarını döndürür.

Başka bir yol, özyinelemeli çağrının herhangi bir ekleme, aritmetik, modifikasyon, vb.

public static int factorial(int mynumber) {
    if (mynumber == 1) {
        return 1;
    } else {            
        return mynumber * factorial(--mynumber);
    }
}

public static int tail_factorial(int mynumber, int sofar) {
    if (mynumber == 1) {
        return sofar;
    } else {
        return tail_factorial(--mynumber, sofar * mynumber);
    }
}

3
0! Yani "mynumber == 1", "mynumber == 0" olmalıdır.
14'te polerto

19

Anlamamın en iyi yolu tail call recursion, son çağrının (veya kuyruk çağrısının) işlevin kendisi olduğu özel bir özyineleme durumudur .

Python'da sağlanan örnekleri karşılaştırmak:

def recsum(x):
 if x == 1:
  return x
 else:
  return x + recsum(x - 1)

^ özyineleme

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

^ KUYRUK TEKRARI

Genel özyinelemeli sürümde görebileceğiniz gibi, kod bloğundaki son çağrı x + recsum(x - 1). Yani recsumyöntemi çağırdıktan sonra , başka bir işlem var x + ...

Bununla birlikte, kuyruk özyinelemeli versiyonda, kod bloğundaki son çağrı (veya kuyruk çağrısı) tailrecsum(x - 1, running_total + x), yöntemin kendisine son çağrı yapıldığı ve bundan sonra hiçbir işlem olmadığı anlamına gelir.

Bu nokta önemlidir, çünkü burada görüldüğü gibi kuyruk özyineleme belleği büyütmemektedir, çünkü alttaki VM kendini kuyruk pozisyonunda (bir fonksiyonda değerlendirilecek son ifade) çağıran bir fonksiyon gördüğünde, mevcut yığın çerçevesini ortadan kaldırır. Kuyruk Çağrısı Optimizasyonu (TCO) olarak bilinir.

DÜZENLE

NB. Yukarıdaki örneğin çalışma zamanı TCO'yu desteklemeyen Python'da yazıldığını unutmayın. Bu sadece konuyu açıklamak için bir örnek. TCO, Scheme, Haskell vb. Dillerde desteklenmektedir


12

Java'da, Fibonacci işlevinin olası bir kuyruk yinelemeli uygulaması:

public int tailRecursive(final int n) {
    if (n <= 2)
        return 1;
    return tailRecursiveAux(n, 1, 1);
}

private int tailRecursiveAux(int n, int iter, int acc) {
    if (iter == n)
        return acc;
    return tailRecursiveAux(n, ++iter, acc + iter);
}

Bunu standart özyinelemeli uygulama ile karşılaştırın:

public int recursive(final int n) {
    if (n <= 2)
        return 1;
    return recursive(n - 1) + recursive(n - 2);
}

1
Bu benim için yanlış sonuçlar döndürüyor, girdi 8 için 36 alıyorum, 21 olmalı. Bir şey eksik mi? Java kullanıyorum ve kopya yapıştırdım.
Alberto Zaccagni

1
Bu, [1, n] içindeki i için TOPLA (i) değerini döndürür. Fibbonacci ile ilgisi yok. Bir Fibbo için, substracts bir testlere ihtiyaç iteriçin accne zaman iter < (n-1).
13'te Askolein

10

Bir Lisp programcı değilim, ama sanırım bu yardımcı olacaktır.

Temel olarak, yinelemeli çağrı yaptığınız son şey olacak şekilde bir programlama tarzıdır.


10

İşte kuyruk özyineleme kullanarak faktöriyeller yapan bir Ortak Lisp örneği. Yığınsız doğa nedeniyle, insan delicesine büyük faktöriyel hesaplamalar yapabilir ...

(defun ! (n &optional (product 1))
    (if (zerop n) product
        (! (1- n) (* product n))))

Ve sonra eğlence için deneyebilirsin (format nil "~R" (! 25))


9

Kısacası, bir kuyruk özyineleme özyinelemeli çağrıyı beklemek zorunda kalmamak için işlevdeki son ifade olarak özyinelemeli çağrıyı içerir.

Yani bu bir kuyruk özyineleme, yani N (x - 1, p * x), derleyicinin bir for döngüye (faktöriyel) optimize edilebileceğini anlamak için akıllı olduğu fonksiyondaki son ifadedir. İkinci parametre p, ara ürün değerini taşır.

function N(x, p) {
   return x == 1 ? p : N(x - 1, p * x);
}

Bu, yukarıdaki faktöryel işlevi yazmanın kuyruk özyinelemesiz yoludur (bazı C ++ derleyicileri yine de optimize edebilir).

function N(x) {
   return x == 1 ? 1 : x * N(x - 1);
}

ama bu değil:

function F(x) {
  if (x == 1) return 0;
  if (x == 2) return 1;
  return F(x - 1) + F(x - 2);
}

" Kuyruk özyineleme anlama - Visual Studio C ++ - montaj görünümü " başlıklı uzun bir yazı yazdım

resim açıklamasını buraya girin


1
N fonksiyonunuz özyinelemeli nasıl?
Fabian Pijcke

N (x-1), derleyicinin bir for-loop (faktöriyel) için optimize edilebileceğini anlamanın akıllı olduğu fonksiyondaki son ifadedir
doctorlai

Benim endişem N fonksiyonunuzun tam olarak bu konunun kabul edilen cevabından fonksiyon özgeçmişi olması (bir ürün değil, bir toplam değil) ve bu özgeçmişin kuyruk özyinelemesiz olduğu söyleniyor mu?
Fabian Pijcke

8

burada tailrecsumdaha önce bahsedilen fonksiyonun Perl 5 versiyonudur .

sub tail_rec_sum($;$){
  my( $x,$running_total ) = (@_,0);

  return $running_total unless $x;

  @_ = ($x-1,$running_total+$x);
  goto &tail_rec_sum; # throw away current stack frame
}

8

Kuyruk özyineleme ile ilgili Bilgisayar Programlarının Yapısı ve Yorumundan bir alıntıdır .

Yinelemenin ve özyinelemenin aksine, özyinelemeli bir süreç kavramını özyinelemeli bir prosedürle karıştırmamaya dikkat etmeliyiz. Bir prosedürü özyinelemeli olarak tanımladığımızda, prosedür tanımının (doğrudan veya dolaylı olarak) prosedürün kendisine atıfta bulunduğu sözdizimsel gerçeğe atıfta bulunuyoruz. Ancak bir süreci, örneğin doğrusal olarak özyinelemeli bir paterni izleyerek tanımladığımızda, bir prosedürün nasıl yazıldığının sözdiziminden değil, sürecin nasıl geliştiğinden bahsediyoruz. Gerçekleştirici gibi yinelemeli bir yordamı yinelemeli bir süreç oluşturmak olarak adlandırmamız rahatsız edici görünebilir. Ancak süreç gerçekten yinelemelidir: Durumu tamamen üç durum değişkeni tarafından yakalanır ve bir tercümanın işlemi gerçekleştirmek için sadece üç değişkeni takip etmesi gerekir.

Süreç ve prosedür arasındaki ayrımın kafa karıştırıcı olabilmesinin bir nedeni, ortak diller (Ada, Pascal ve C dahil) uygulamalarının çoğunun, özyinelemeli prosedürlerin yorumlanmasının, prensip olarak, tekrarlanan işlem yinelemeli olsa bile, prosedür çağrılarının sayısı. Sonuç olarak, bu diller yinelemeli süreçleri ancak do, repeat, until, for ve while gibi özel amaçlı “döngü yapılarına” başvurarak tanımlayabilir. Şema'nın uygulanması bu kusuru paylaşmaz. Yinelemeli işlem özyinelemeli bir yordamla tanımlanmış olsa bile, sabit alanda yinelemeli bir işlem yürütür. Bu özelliğe sahip bir uygulamaya kuyruk yinelemeli denir. Kuyruk özyinelemeli bir uygulama ile, yineleme sıradan prosedür çağrı mekanizması kullanılarak ifade edilebilir, böylece özel yineleme yapıları sadece sözdizimsel şeker olarak faydalıdır.


1
Burada tüm cevapları okudum ve yine de bu, bu kavramın gerçekten derin çekirdeğine dokunan en açık açıklama. Her şeyi bu kadar basit ve net görecek şekilde düz bir şekilde açıklar. Kabalık durumumu affet lütfen. Her nasılsa diğer cevaplar kafasına çiviyi vurma gibi hissettiriyor. SICP'nin bu yüzden önemli olduğunu düşünüyorum.
englealuze

8

Özyinelemeli işlev, kendi kendine çağrılan bir işlevdir

Programcıların minimum miktarda kod kullanarak verimli programlar yazmasına olanak tanır .

Dezavantajı, düzgün yazılmadığı takdirde sonsuz döngülere ve diğer beklenmedik sonuçlara neden olabilmeleridir .

Hem Basit Yinelemeli işlevi hem de Kuyruk Yinelemeli işlevini açıklayacağım

Basit bir özyinelemeli işlev yazmak için

  1. Dikkate alınması gereken ilk nokta, if döngüsü olan döngüden çıkmaya ne zaman karar vermeniz gerektiğidir.
  2. İkincisi, eğer kendi fonksiyonumuzsa ne yapacağımız süreçtir

Verilen örnekten:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

Yukarıdaki örnekten

if(n <=1)
     return 1;

Döngüden ne zaman çıkılacağına karar veren faktör mü

else 
     return n * fact(n-1);

Gerçek işlem yapılacak mı?

Kolay anlaşılması için görevi tek tek kırmama izin verin.

Eğer koşarsam dahili olarak neler olduğunu görelim fact(4)

  1. İkame n = 4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

Ifdöngü başarısız olur, böylece döngü olur, böylece elsegeri döner4 * fact(3)

  1. Yığın hafızasında, 4 * fact(3)

    İkame n = 3

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

Ifo gider böylece döngü başarısız elsedöngü

böylece geri dönüyor 3 * fact(2)

Unutmayın `` 4 * gerçek (3) ''

İçin çıktı fact(3) = 3 * fact(2)

Şimdiye kadar yığın 4 * fact(3) = 4 * 3 * fact(2)

  1. Yığın hafızasında, 4 * 3 * fact(2)

    İkame n = 2

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

Ifo gider böylece döngü başarısız elsedöngü

böylece geri dönüyor 2 * fact(1)

Aradığımızı hatırla 4 * 3 * fact(2)

İçin çıktı fact(2) = 2 * fact(1)

Şimdiye kadar yığın 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. Yığın hafızasında, 4 * 3 * 2 * fact(1)

    İkame n = 1

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If döngü doğru

böylece geri dönüyor 1

Aradığımızı hatırla 4 * 3 * 2 * fact(1)

İçin çıktı fact(1) = 1

Şimdiye kadar yığın 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

Son olarak, gerçeğin sonucu (4) = 4 * 3 * 2 * 1 = 24

resim açıklamasını buraya girin

Kuyruk Özyineleme olurdu

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}

  1. İkame n = 4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

Ifdöngü başarısız olur, böylece döngü olur, böylece elsegeri dönerfact(3, 4)

  1. Yığın hafızasında, fact(3, 4)

    İkame n = 3

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

Ifo gider böylece döngü başarısız elsedöngü

böylece geri dönüyor fact(2, 12)

  1. Yığın hafızasında, fact(2, 12)

    İkame n = 2

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

Ifo gider böylece döngü başarısız elsedöngü

böylece geri dönüyor fact(1, 24)

  1. Yığın hafızasında, fact(1, 24)

    İkame n = 1

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If döngü doğru

böylece geri dönüyor running_total

İçin çıktı running_total = 24

Son olarak, gerçeğin sonucu (4,1) = 24

resim açıklamasını buraya girin


7

Kuyruk özyineleme şu anda yaşadığınız hayattır. Aynı yığın çerçevesini sürekli olarak geri dönüştürürsünüz, çünkü bir "önceki" çerçeveye geri dönmek için hiçbir neden veya araç yoktur. Atılmak için geçmiş bitti ve bitti. Süreciniz kaçınılmaz olarak ölünceye kadar sonsuza dek geleceğe doğru giden bir çerçeve elde edersiniz.

Bazı işlemlerin ek kareler kullanabileceğini düşündüğünüzde analoji bozulur, ancak yığın sonsuza kadar büyümezse yine de kuyruk yinelemeli olarak kabul edilir.


1
bölünmüş kişilik bozukluğu yorumu altında kırılmaz . :) Zihin Toplumu ; Toplum Olarak Bir Zihin. :)
Will Ness

Vaov! Şimdi düşünmek için başka bir yol
sutanu dalui

7

Kuyruk özyineleme, özyinelemeli çağrının dönüşünden sonra hiçbir hesaplama yapılmadığı işlevin sonunda ("kuyruk") kendisini çağırdığı özyinelemeli bir işlevdir. Birçok derleyici, özyinelemeli bir çağrıyı kuyruk özyinelemeli veya yinelemeli bir çağrıyla değiştirmeyi en iyi duruma getirir.

Bir sayının hesaplama faktöriyelini düşünün.

Basit bir yaklaşım şöyle olacaktır:

  factorial(n):

    if n==0 then 1

    else n*factorial(n-1)

Faktöriyel (4) dediğinizi varsayalım. Özyineleme ağacı:

       factorial(4)
       /        \
      4      factorial(3)
     /             \
    3          factorial(2)
   /                  \
  2                factorial(1)
 /                       \
1                       factorial(0)
                            \
                             1    

Yukarıdaki durumda maksimum özyineleme derinliği O (n) 'dir.

Ancak, aşağıdaki örneği göz önünde bulundurun:

factAux(m,n):
if n==0  then m;
else     factAux(m*n,n-1);

factTail(n):
   return factAux(1,n);

Gerçekler için özyineleme ağacı (4) şöyle olur:

factTail(4)
   |
factAux(1,4)
   |
factAux(4,3)
   |
factAux(12,2)
   |
factAux(24,1)
   |
factAux(24,0)
   |
  24

Burada da, maksimum özyineleme derinliği O (n) 'dir, ancak çağrıların hiçbiri yığına ekstra değişken eklemez. Böylece derleyici bir yığınla uzaklaşabilir.


7

Kuyruk Özyineleme normal özyineleme ile karşılaştırıldığında oldukça hızlıdır. Hızlıdır, çünkü ataların çağrısının çıktısı, izi tutmak için yığına yazılmaz. Ancak normal özyinelemede tüm atalar yolu tutmak için yığına yazılan çıktıyı çağırır.


6

Bir kuyruk özyinelemeli fonksiyon dönmeden önce öyle son işlem özyinelemeli fonksiyon çağrısı yapmak bir özyinelemeli fonksiyonudur. Yani, özyinelemeli işlev çağrısının dönüş değeri hemen döndürülür. Örneğin, kodunuz şöyle görünür:

def recursiveFunction(some_params):
    # some code here
    return recursiveFunction(some_args)
    # no code after the return statement

Kuyruk çağrısı optimizasyonu veya kuyruk çağrısı eliminasyonu uygulayan derleyiciler ve yorumlayıcılar, yığın taşmalarını önlemek için yinelemeli kodu optimize edebilir. Derleyiciniz veya yorumcunuz kuyruk çağrısı optimizasyonu (CPython yorumlayıcısı gibi) uygulamıyorsa, kodunuzu bu şekilde yazmanın başka bir yararı yoktur.

Örneğin, bu Python'da standart bir özyinelemeli faktöryel işlevdir:

def factorial(number):
    if number == 1:
        # BASE CASE
        return 1
    else:
        # RECURSIVE CASE
        # Note that `number *` happens *after* the recursive call.
        # This means that this is *not* tail call recursion.
        return number * factorial(number - 1)

Ve bu faktöriyel fonksiyonun bir kuyruk çağrısı özyinelemeli versiyonu:

def factorial(number, accumulator=1):
    if number == 0:
        # BASE CASE
        return accumulator
    else:
        # RECURSIVE CASE
        # There's no code after the recursive call.
        # This is tail call recursion:
        return factorial(number - 1, number * accumulator)
print(factorial(5))

(Bu Python kodu olmasına rağmen, CPython yorumlayıcısının kuyruk çağrısı optimizasyonu yapmadığını unutmayın, bu nedenle kodunuzu bu şekilde düzenlemek çalışma zamanı avantajı sağlamaz.)

Faktöriyel örnekte gösterildiği gibi, kuyruk çağrısı optimizasyonunu kullanmak için kodunuzu biraz daha okunmaz hale getirmeniz gerekebilir. (Örneğin, temel durum artık biraz sezgisel değildir ve accumulatorparametre bir tür küresel değişken olarak etkili bir şekilde kullanılmaktadır.)

Ancak kuyruk çağrı optimizasyonunun yararı, yığın taşması hatalarını önlemesidir. (Yinelenen bir algoritma yerine yinelemeli bir algoritma kullanarak da aynı avantajı elde edeceğinizi not edeceğim.)

Yığın taşmaları, çağrı yığını çok fazla çerçeve nesnesinin üzerine itildiğinde ortaya çıkar. Bir işlev çağrıldığında çerçeve nesnesi çağrı yığınına itilir ve işlev döndüğünde çağrı yığınından çıkar. Çerçeve nesneleri, yerel değişkenler ve işlev döndüğünde hangi kod satırının döndürüleceği gibi bilgiler içerir.

Yinelemeli işleviniz geri dönmeden çok fazla yinelemeli çağrı yaparsa, çağrı yığını çerçeve nesnesi sınırını aşabilir. (Sayı platforma göre değişir; Python'da varsayılan olarak 1000 çerçeve nesnesidir.) Bu, bir yığın taşmasına neden olur . (Hey, bu web sitesinin adı buradan geliyor!)

Ancak, özyinelemeli işlevinizin yaptığı son şey, özyinelemeli çağrı yapmak ve dönüş değerini döndürmekse, geçerli kare nesnesinin çağrı yığınında kalması için gerek yoktur. Sonuçta, özyinelemeli işlev çağrısından sonra kod yoksa, geçerli çerçeve nesnesinin yerel değişkenlerine takılmak için bir neden yoktur. Böylece mevcut kare nesnesinden çağrı yığınında tutmak yerine derhal kurtulabiliriz. Bunun sonucu, çağrı yığınınızın boyutunun büyümemesi ve bu nedenle taşmayı yığılamamasıdır.

Bir derleyici veya yorumlayıcı, kuyruk çağrısı optimizasyonunun ne zaman uygulanabileceğini tanıyabilmesi için kuyruk çağrısı optimizasyonuna sahip olmalıdır. O zaman bile, kuyruk çağrısı optimizasyonunu kullanmak için özyinelemeli işlevinizdeki kodu yeniden düzenleyebilirsiniz ve okunabilirlikteki bu potansiyel düşüşün optimizasyona değip değmeyeceği size bağlıdır.


Msgstr "Kuyruk özyineleme (kuyruk çağrısı optimizasyonu veya kuyruk çağrısının ortadan kaldırılması da denir)". Hayır; kuyruk çağrısı eleme veya kuyruk çağrısı optimizasyonu, kuyruk özyinelemeli işleve uygulayabileceğiniz bir şeydir , ancak bunlar aynı şey değildir. Python'da kuyruk yinelemeli işlevler (gösterdiğiniz gibi) yazabilirsiniz, ancak kuyruk yinelemeli olmayan bir işlevden daha verimli değildir, çünkü Python kuyruk çağrısı optimizasyonu gerçekleştirmez.
chepner

Birisi web sitesini optimize etmeyi ve özyinelemeli çağrı kuyruk özyinelemeyi oluşturmayı başarırsa, artık StackOverflow sitesine sahip olmayacağımız anlamına mı geliyor ?! Bu korkunç.
Nadjib Mami

5

Kuyruk çağrısı özyineleme ve kuyruk çağrısı özyineleme arasındaki temel farklardan bazılarını anlamak için bu tekniklerin .NET uygulamalarını araştırabiliriz.

İşte C #, F # ve C ++ \ CLI'de bazı örneklere sahip bir makale: C #, F # ve C ++ \ CLI'de Kuyruk Özyineleme Maceraları .

C # kuyruk çağrısı özyineleme için optimize etmezken F # yapar.

İlke arasındaki farklar döngülere karşı Lambda hesabıdır. C #, döngüler göz önünde bulundurularak tasarlanırken F #, Lambda hesabı ilkelerinden oluşturulmuştur. Lambda hesabının ilkeleri hakkında çok iyi (ve ücretsiz) bir kitap için bkz . Abelson, Sussman ve Sussman'ın Bilgisayar Programlarının Yapısı ve Yorumu .

F # 'daki kuyruk çağrıları ile ilgili olarak, çok iyi bir tanıtım makalesi için bkz . F #' daki Kuyruk Çağrılarına Detaylı Giriş . Son olarak, kuyruk olmayan özyineleme ve kuyruk çağrısı özyineleme (F # 'da) arasındaki farkı kapsayan bir makale: F keskinliğinde kuyruk özyineleme ve kuyruk olmayan özyineleme .

C # ve F # arasındaki kuyruk çağrısı özyinelemesinin tasarım farklılıklarından bazılarını okumak isterseniz, bkz . C # ve F #'da Kuyruk Çağrısı Opcode'u Oluşturma .

Hangi koşulların C # derleyicisinin kuyruk çağrısı optimizasyonları gerçekleştirmesini engellediğini bilmek istiyorsanız, bu makaleye bakın: JIT CLR kuyruk çağrısı koşulları .


4

İki temel özyineleme türü vardır: kafa özyineleme ve kuyruk özyineleme.

In kafa özyineleme , bir işlevi özyinelemeli çağrı yapar ve sonra belki örneğin özyinelemeli çağrısının sonucunu kullanarak, biraz daha hesaplamalar gerçekleştirir.

Bir de kuyruk özyinelemeli fonksiyon, bütün hesaplamalar ilk gerçekleşmesi ve özyinelemeli çağrı olur son şeydir.

Alındığı bu süper harika yazı. Lütfen okumayı düşünün.


4

Özyineleme, kendisini çağıran bir işlev anlamına gelir. Örneğin:

(define (un-ended name)
  (un-ended 'me)
  (print "How can I get here?"))

Kuyruk-Özyineleme işlevi sona erdiren özyineleme anlamına gelir:

(define (un-ended name)
  (print "hello")
  (un-ended 'me))

Bakın, sonlanmamış fonksiyon (Şema jargonundaki prosedür) kendisini çağırmaktır. Başka bir (daha yararlı) örnek:

(define (map lst op)
  (define (helper done left)
    (if (nil? left)
        done
        (helper (cons (op (car left))
                      done)
                (cdr left))))
  (reverse (helper '() lst)))

Yardımcı prosedürde, sol nil değilse yaptığı LAST şey kendini çağırmaktır (bir şey eksilerini ve cdr bir şey SONRA). Temel olarak bir listeyi nasıl eşlediğinizdir.

Kuyruk özyineleme, yorumlayıcının (veya dile ve satıcıya bağlı derleyicinin) optimize edebilmesi ve bir while döngüsüne eşdeğer bir şeye dönüştürebilmesi için büyük bir avantaja sahiptir. Aslında, Şema geleneğinde, çoğu "için" ve "while" döngüsü kuyruk-özyineleme tarzında yapılır (bildiğim kadarıyla ve süre için yoktur).


3

Bu sorunun birçok harika yanıtı var ... ama yardım edemem ama "kuyruk özyineleme" veya en azından "uygun kuyruk özyineleme" nin nasıl tanımlanacağına dair alternatif bir yaklaşımla karşılaşıyorum. Yani: bir programdaki belirli bir ifadenin özelliği olarak bakmalı mıdır? Yoksa buna bir programlama dilinin uygulanmasının bir özelliği olarak bakılmalı mıdır?

İkinci görünümde daha fazla bilgi için, Will Clinger tarafından "Uygun Kuyruk Özyineleme ve Alan Verimliliği" (PLDI 1998) adlı klasik bir makale , "uygun kuyruk özyinelemesini" bir programlama dili uygulamasının bir özelliği olarak tanımlamıştır. Tanım, bir kişinin uygulama ayrıntılarını göz ardı etmesine izin vermek için yapılandırılmıştır (çağrı yığınının gerçekte çalışma zamanı yığını üzerinden mi yoksa yığınla ayrılmış bağlantılı bir çerçeve listesi aracılığıyla mı temsil edildiği gibi).

Bunu başarmak için asimptotik analiz kullanır: program yürütme süresinin genellikle gördüğü gibi değil, program alanı kullanımını kullanır . Bu şekilde, bir yığın çalışma ayrılmış bağlantı listesinin bir çalışma zamanı çağrı yığınına karşı alan kullanımı asimptotik olarak eşdeğer olur; bu yüzden programlama dili uygulama detayını (uygulamada kesinlikle biraz önemli olan ancak belirli bir uygulamanın "mülkiyet kuyruğu özyinelemeli" gereksinimini karşılayıp karşılamadığını belirlemeye çalıştığında suları biraz çamurlayabilen bir detay göz ardı edilir. )

Bu makale çeşitli nedenlerden dolayı dikkatle incelenmeye değer:

  • Bir programın kuyruk ifadelerinin ve kuyruk çağrılarının endüktif bir tanımını verir . (Böyle bir tanım ve bu tür çağrıların neden önemli olduğu, burada verilen diğer cevapların çoğunun konusu gibi görünmektedir.)

    İşte bu tanımlar, sadece metnin bir lezzetini sağlamak için:

    Tanım 1 kuyruk ifadeler Çekirdek Şema yazılmış bir program tanımlandığı tümevarımsal olarak izler.

    1. Bir lambda ifadesinin gövdesi bir kuyruk ifadesidir
    2. Eğer (if E0 E1 E2)her iki, bir kuyruk ifadesidir E1ve E2kuyruk ifadelerdir.
    3. Başka hiçbir şey bir kuyruk ifadesi değildir.

    Tanım 2 bir kuyruk arama bir prosedür çağrı kuyruk ifadesidir.

(bir kuyruk özyinelemeli çağrı veya makalenin dediği gibi "kendi kuyruğu çağrısı", prosedürün kendisinin çağrıldığı bir kuyruk çağrısının özel bir durumudur.)

  • Çekirdek Şemasını değerlendirmek için altı farklı "makine" için, her bir makinenin içinde bulunduğu asimtotik uzay karmaşıklık sınıfı dışında aynı gözlemlenebilir davranışa sahip olduğu resmi tanımlamalar sağlar .

    Örneğin, sırasıyla 1. yığın tabanlı bellek yönetimi, 2. çöp toplama ancak kuyruk çağrıları yok, 3. çöp toplama ve kuyruk çağrıları olan makineler için tanımlar verdikten sonra, kağıt daha gelişmiş depolama yönetimi stratejileriyle devam eder. 4. "evlis kuyruk özyineleme", burada bir kuyruk çağrısında son alt ifade argümanının değerlendirilmesi boyunca çevrenin korunmasına ihtiyaç duyulmadığı, 5. bir kapama ortamının o kapamanın sadece serbest değişkenlerine indirgenmesi ve 6. Appel ve Shao tarafından tanımlanan "alan için güvenli" anlambilim .

  • Makinelerin aslında altı farklı uzay karmaşıklık sınıfına ait olduğunu kanıtlamak için, karşılaştırılan her bir makine çifti için kağıt, bir makinede asimtotik alan patlamasını açığa çıkaracak, diğerinde değil programların somut örneklerini sunmaktadır.


(Şimdi cevabımı okurken, Clinger belgesinin önemli noktalarını gerçekten yakalayıp yakalamadığımı bilmiyorum. Ancak, ne yazık ki, şu anda bu cevabı geliştirmek için daha fazla zaman ayıramıyorum.)


1

Birçok kişi burada özyinelemeyi zaten açıkladı. Riccardo Terrell'in “.NET'te Eşzamanlılık, Eşzamanlı ve paralel programlamanın modern modelleri” kitabından özyinelemenin sağladığı bazı avantajlar hakkında birkaç düşünceden bahsetmek istiyorum:

“Fonksiyonel özyineleme FP'de yinelemenin doğal yoludur, çünkü durum mutasyonunu önler. Her yineleme sırasında, döngü yapıcısına güncellenmek (değiştirilmek) yerine yeni bir değer iletilir. Buna ek olarak, programınızı daha modüler hale getiren ve paralellikten faydalanma fırsatlarını tanıtan, yinelemeli bir işlev oluşturulabilir. "

İşte aynı kitaptan kuyruk özyineleme hakkında bazı ilginç notlar:

Kuyruk çağrısı özyineleme, düzenli bir özyinelemeli işlevi herhangi bir risk ve yan etki olmadan büyük girdileri işleyebilen optimize edilmiş bir sürüme dönüştüren bir tekniktir.

NOT Bir kuyruk çağrısının optimizasyon olarak temel nedeni veri yerini, bellek kullanımını ve önbellek kullanımını geliştirmektir. Kuyruk çağrısı yaparak, arayan kişi arayanla aynı yığın alanını kullanır. Bu, bellek basıncını azaltır. Aynı bellek sonraki arayanlar için yeniden kullanıldığından ve yeni bir önbellek hattına yer açmak için daha eski bir önbellek satırını çıkarmak yerine önbellekte kalabileceğinden önbelleği marjinal olarak iyileştirir.

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.