1. 程式人生 > 實用技巧 >函式優化之尾呼叫實踐

函式優化之尾呼叫實踐

參考維基百科對“尾呼叫”定義:

電腦科學裡,尾呼叫是指一個函式裡的最後一個動作是一個函式呼叫的情形:即這個呼叫的返回值直接被當前函式返回的情形。這種情形下稱該呼叫位置為尾位置。若這個函式在尾位置呼叫本身(或是一個尾呼叫本身的其他函式等等),則稱這種情況為尾遞迴,是遞迴的一種特殊情形。尾呼叫不一定是遞迴呼叫,但是尾遞迴特別有用,也比較容易實現。

在程式執行時,計算機會為應用程式分配一定的記憶體空間;應用程式則會自行分配所獲得的記憶體空間,其中一部分被用於記錄程式中正在呼叫的各個函式的執行情況,這就是函式的呼叫棧。常規的函式呼叫總是會在呼叫棧最上層新增一個新的堆疊幀(stack frame,也翻譯為“棧幀”或簡稱為“幀”),這個過程被稱作“入棧”或“壓棧”(意即把新的幀壓在棧頂)。當函式的呼叫層數非常多時,呼叫棧會消耗不少記憶體,甚至會撐爆記憶體空間(

棧溢位[1],造成程式嚴重卡頓或意外崩潰。尾呼叫的呼叫棧則特別易於優化,從而可減少記憶體空間的使用,也能提高執行速度。[1]其中,對尾遞迴情形的優化效果最為明顯,尤其是遞迴演算法非常複雜的情形。[1]

一般來說,尾呼叫消除是可選的,可以用,也可以不用。然而,在函式程式語言中,語言標準通常會要求編譯器或執行平臺實現尾呼叫消除。這讓程式設計師可以用遞迴取代迴圈而不喪失效能。

這裡還是以php語言來做示例演示。

factorial.php

[root@guangzhou tmp_dir]# cat factorial.php 
<?php

function factorial($n) {
    
if ($n == 0) { return 1; } return factorial($n - 1) * $n; } $start = microtime(true); echo '記憶體使用量1:' . memory_get_usage() . PHP_EOL; echo '執行結果:' . factorial(80000) . PHP_EOL; echo '執行耗時:' . (microtime(true) - $start) . 's' . PHP_EOL; echo '記憶體使用量2::' . memory_get_usage() . PHP_EOL
; echo '記憶體使用峰值:' . memory_get_peak_usage() . PHP_EOL; [root@guangzhou tmp_dir]# php factorial.php 記憶體使用量1:700840 執行結果:INF 執行耗時:0.010078907012939s 記憶體使用量2::700872 記憶體使用峰值:13283784

factorial2.php

[root@guangzhou tmp_dir]# cat factorial2.php 
<?php

function factorial($n, $total=1){
   if($n == 1)
   {
       return 1;
   }else{
       return factorial($n-1, $total*$n);
   }
}

$start = microtime(true);

echo '記憶體使用量1:' . memory_get_usage() . PHP_EOL;

echo '執行結果:' . factorial(80000) . PHP_EOL;

echo '執行耗時:' . (microtime(true) - $start)  . 's' . PHP_EOL;

echo '記憶體使用量2::' . memory_get_usage() . PHP_EOL;

echo '記憶體使用峰值:' . memory_get_peak_usage() . PHP_EOL;
[root@guangzhou tmp_dir]# php factorial2.php 
記憶體使用量1:701096
執行結果:1
執行耗時:0.01151704788208s
記憶體使用量2::701128
記憶體使用峰值:14594760

後面將$n=80000改成$n=800000,報錯記憶體溢位,後面查詢網上資料,發現php的尾呼叫需要換種方式來編寫。

這裡直接執行$n=800000000

factorial3.php

[root@guangzhou tmp_dir]# cat factorial3.php 
<?php

function factorial($n, $accumulator = 1) {
    if ($n == 0) {
        return $accumulator;
    }

    return function() use($n, $accumulator) {
        return factorial($n - 1, $accumulator * $n);
    };
}

function trampoline($callback, $params) {
    $result = call_user_func_array($callback, $params);

    while (is_callable($result)) {
        $result = $result();
    }

    return $result;
}

$start = time();

echo '記憶體使用量1:' . memory_get_usage() . PHP_EOL;

echo '執行結果:' . trampoline('factorial', array(80000000)) . PHP_EOL;

echo '執行耗時:' . (time() - $start)  . 's' . PHP_EOL;

echo '記憶體使用量2::' . memory_get_usage() . PHP_EOL;

echo '記憶體使用峰值:' . memory_get_peak_usage() . PHP_EOL;
[root@guangzhou tmp_dir]# php factorial3.php 
記憶體使用量1:703104
執行結果:INF
執行耗時:20s
記憶體使用量2::703136
記憶體使用峰值:807304

可見記憶體使用不多,峰值也只是增加10kb左右。