Rust程式語言融合了多種程式設計法(programming paradigm),其中的指令式程式設計(imperative programming)所用的迴圈和函數式程式設計(functional programming)所提供的迭代器(iterator)可以加強陣列或是切片的走訪效能。然而,很多時候我們並不需要從頭走訪陣列或是切片,這時可以利用Rust提供的陣列轉切片或是切片再切片的功能來進行索引值的位移,事實上迭代器也有提供skip方法,可以在跳過從前面算起的指定次數的迭代處理程序後,後面的迭代程序才會真正地開始被處理。那如果迭代器的走訪對象是陣列或是切片時,使用skip方法會比直接使用更小範圍的切片還要慢嗎?



我們先來看看以下程式碼:

let array = [2, 2, 3, 4, 5, 6, 7, 8];

for i in 5..array.len() {
    println!("{}", array[i]);
}

以上程式,會利用For計次迴圈來從索引值5開始走訪array這個存在於堆疊內的陣列,並將走訪到的元素值印在螢幕上。

先前的文章中,我們已經知道用For迴圈來走訪堆疊內的陣列,效能跟和用For迭代器迴圈或是迭代器是一樣的。

所以,以上程式其實也會等效於以下的For迭代器迴圈程式:

let array = [2, 2, 3, 4, 5, 6, 7, 8];

for &n in array[5..].iter() {
    println!("{}", n);
}

以上程式,利用Rust提供的陣列轉切片功能,直接從陣列的索引值5開始走訪陣列。那如果我們不使用切片,而是用迭代器提供的skip方法來完成這個在走訪陣列時略過前面幾個元素的功能呢?程式如下:

let array = [2, 2, 3, 4, 5, 6, 7, 8];

for &n in array.skip(5) {
    println!("{}", n);
}

使用skip方法所二度產生出來的迭代器,會影響到效能嗎?

效能實測

直接實際寫一段程式來測試運算效能吧!這段程式可以在GitHub上取得:

根據測試結果,可以發現使用skip方法的效能明顯比使用切片還要差。這是因為skip方法所產生的Skip結構實體,在第一次迭代被呼叫next方法時,因為n在經過if條件式判斷之後會發現不等於0,所以會再去呼叫nth方法,程式碼如下:

fn next(&mut self) -> Option<I::Item> {
    if self.n == 0 {
        self.iter.next()
    } else {
        let old_n = self.n;
        self.n = 0;
        self.iter.nth(old_n)
    }
}

而這個迭代器的nth方法究竟會做什麼事,還得看迭代器本身的實作方式。預設的nth方法實作方式是一邊走訪一邊略過指定的元素數量,所以雖然看起來陣列前面的元素並沒有進入迭代程序,但其實還是有被走訪到!nth方法的程式碼如下:

fn nth(&mut self, mut n: usize) -> Option<Self::Item> {
    for x in self {
        if n == 0 { return Some(x) }
        n -= 1;
    }
    None
}

不過如果是切片產生出來的迭代器,nth方法有著它專屬的實作方式,程式碼如下:

fn nth(&mut self, n: usize) -> Option<$elem> {
    if n >= len!(self) {
        if mem::size_of::<T>() == 0 {
            self.end = self.ptr;
        } else {
            self.ptr = self.end;
        }
        return None;
    }
    unsafe {
        let elem = Some(& $( $mut_ )* *self.ptr.add(n));
        self.post_inc_start((n as isize).wrapping_add(1));
        elem
    }
}

以上其實是寫在巨集中的程式,我們可以看出Rust的切片是直接利用指標來進行記憶體位址的位移。這樣的方式其實還算不錯,但實際運行起來的效能卻慘輸另外兩種方式,或許跟編譯器優化有關,不曉得。總之,skip方法還是儘量少用為妙。