[rust] 13. 함수형 언어의 특성: 반복자와 클로저

About_work·2024년 8월 16일
0

rust

목록 보기
15/16

1. 클로저: 자신의 환경을 캡처하는 익명 함수

  • 함수형 스타일의 프로그래밍은 대개 아래의 것들을 포함 (아래의 것들을 closure라고 한다.)
    • 함수를 값처럼 인수로 넘기는 것,
    • 다른 함수들에서 결괏값으로 함수들을 반환하는 것,
    • 나중에 실행하기 위해 함수를 변수에 할당하는 것

1.1. 클로저로 환경 캡처하기

  • 손님이 원하는 셔츠 색이 있으면 그 셔츠를 주고, 손님이 원하는 셔츠가 없으면 재고 중 많이 남은 색의 셔츠를 주는 코드
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}
  • Option<T>의 unwrap_or_else 메서드
    • 이 메서드는 하나의 인수를 받습니다:
      • 바로 아무런 인수도 없고 T 값을 반환하는 클로저 입니다.
        • 이때 T는 Option<T>의 Some 배리언트에 저장되는 타입과 동일하며, 지금의 경우 ShirtColor입니다.
        • 클로저의 인수는 || 기호 안에 들어갑니다.
        • 이 클로저는 self Inventory 인스턴스의 불변 참조자를 캡처하여 우리가 지정한 코드와 함께 이 값을 unwrap_or_else 메서드에 넘겨줍니다.
        • 반면에 함수는 이런 방식으로 자신의 환경을 캡처할 수 없습니다.

1.2. 클로저 타입 추론과 명시

  • 클로저는 보통 매개변수 혹은 반환 값의 타입을 명시하도록 요구하지 않습니다.
  • 클로저는 함수처럼 노출된 인터페이스로 사용되지 않습니다:
    • 클로저는 이름이 지어지거나 라이브러리의 사용자들에게 노출되지 않은 채로 변수에 저장되고 사용됩니다.
  • 클로저는 통상적으로 짧고, (임의의 시나리오가 아니라) 짧은 컨텍스트 내에서만 관련됩니다.
  • 컴파일러는 (대부분의 변수에 대한 타입을 추론하는 방법과 비슷한 식으로) 클로저의 매개변수와 반환 타입을 추론합니다.
    • 컴파일러가 클로저 타입을 명시하도록 요구하는 경우도 드물게는 있습니다.
  • 더 장황해지더라도 명시성과 명확성을 올리고 싶다면 타입 명시를 추가할 수 있습니다.
  • add_one_v3와 add_one_v4 줄을 컴파일하기 위해서는 이 클로저들이 평가되는 곳이 필요한데,
    • 그 이유는 이 클로저들이 사용된 곳에서 타입이 추론될 것이기 때문입니다.
    • 이는 let v = Vec::new();가 러스트에 의해 타입이 추론되기 위해서
      • 타입 명시 혹은 Vec 안에 집어넣을 어떤 타입의 값이 필요한 것과 유사
  • 클로저 정의에 대하여, 컴파일러는 각각의 매개변수와 반환 값마다 하나의 고정 타입을 추론할 것
  • 아래 코드는 에러남
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);

1.3. 참조자를 캡처하거나 소유권 이동하기

  • 클로저는 세 가지 방식으로 자신의 환경으로부터 값을 캡처할 수 있는데, 이는 함수가 매개변수를 취하는 세 가지 방식과 직접적으로 대응됩니다:
    • 불변으로 빌려오기,
    • 가변으로 빌려오기,
    • 소유권 이동
  • 클로저는 캡처된 값이 쓰이는 방식에 기초하여 캡처할 방법을 결정
  • list에 대한 여러 개의 불변 참조자를 동시에 가질 수 있기 때문에, list에는 클로저 정의 전이나 후 뿐만 아니라 클로저의 호출 전과 후에도 여전히 접근이 가능합니다

  • borrows_mutably가 정의된 시점에, 이 클로저가 list에 대한 가변 참조자를 캡처합니다.
  • 클로저가 호출된 이후로 다시 클로저를 사용하고 있지 않으므로, 가변 대여가 그 시점에서 끝납니다.
  • 클로저 정의와 호출 사이에는 출력을 위한 불변 대여가 허용되지 않는데, 이는 가변 대여가 있을 때는 다른 대여가 허용되지 않기 때문입니다.

  • 클로저의 본문에서 사용하고 있는 값의 소유권이 필요하진 않더라도,
    • 만약 여러분이 클로저가 소유권을 갖도록 만들고 싶다면, 매개변수 리스트 전에 move 키워드를 사용할 수 있습니다.
    • 클로저를 새 스레드에 넘길 때, 데이터를 이동시켜서 새로운 스레드가 이 데이터를 소유하게 하는 경우 유용
use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    thread::spawn(move || println!("From thread: {:?}", list))
        .join()
        .unwrap();
}
  • 이 예제에서는 클로저 본문이 여전히 불변 참조자만 필요할지라도, 클로저 정의의 앞부분에 move 키워드를 집어넣어 list가 이동되어야 함을 명시할 필요가 있습니다.
  • 만일 메인 스레드가 list의 소유권을 유지하고 있는데 (새 스레드가 끝나기 전에 끝나버려서) list를 제거한다면, 새 스레드의 불변 참조자는 유효하지 않게 될 것입니다.

1.4. 캡처된 값을 클로저 밖으로 이동하기와 Fn 트레이트

  • 클로저 본문은 다음의 것들을 할 수 있습니다:
    • 시작 단계에서부터 환경으로부터 아무 값도 캡처하지 않기 (1)
    • 캡처된 값을 이동시키지도 변형시키지도 않기 (2)
    • 캡처된 값을 변형하기 (3)
    • 캡처된 값을 클로저 밖으로 이동시키기 (4)
  • 클로저가 환경으로부터 값을 캡처하고 다루는 방식(위 4가지)이 클로저가 구현하는 트레이트에 영향을 줌
    • 클로저는 "클로저의 본문이 값을 처리하는 방식(위 4가지)에 따라서"
      • 아래 Fn 트레이트들 중 "하나, 둘, 혹은 셋 모두"를 추가하는 방식으로 자동으로 구현
  • 클로저가 구현한 트레이트함수와 구조체가 사용할 수 있는 클로저의 종류를 명시할 수 있는 방법

  • FnOnce
    • 한 번만 호출될 수 있는 클로저에게 적용
    • 모든 클로저들은 호출될 수 있으므로, 최소한 이 트레이트는 구현해 둡니다.
    • 캡처된 값을 본문 밖으로 이동시키는 클로저(4)에 대해서는 FnOnce만 구현되며 나머지 Fn 트레이트는 구현되지 않는데,
      • 이는 이 클로저가 딱 한 번만 호출될 수 있기 때문입니다.
  • FnMut
    • 본문 밖으로 캡처된 값을 이동시키지는 않지만, 값을 변경할 수는 있는 클로저(3)에 대해 적용
    • 이러한 클로저는 한 번 이상 호출될 수 있습니다.
  • Fn
    • 캡처된 값을 본문 밖으로 이동시키지 않고 캡처된 값을 변경하지도 않는 클로저(2)는 물론, 환경으로부터 아무런 값도 캡처하지 않는 클로저(1)에 적용
    • 이러한 클로저는 자신의 환경을 변경시키지 않으면서 한번 이상 호출될 수 있는데, 이는 클로저가 동시에 여러 번 호출되는 등의 경우에서 중요
  • 참고: T가 Option의 Some 배리언트 내 값의 타입을 나타내는 제네릭 타입임을 상기
  • 제네릭 타입 F에 명시된 트레이트 바운드는 FnOnce() -> T인데,
    • 이는 F가 한 번만 호출될 수 있어야 하고, 인수가 없고, T를 반환함을 의미
  • 트레이트 바운드에 FnOnce를 사용하는 것은 unwrap_or_else가 f를 아무리 많아야 한 번만 호출할 것이라는 제약 사항을 표현해 줍니다.
  • 모든 클로저가 FnOnce를 구현하므로 unwrap_or_else는 가장 다양한 종류의 클로저를 허용하며 될 수 있는 한 유연하게 동작합니다.

  • 함수도 이 세 종류의 Fn 트레이트를 모두 구현할 수 있습니다.
  • 만일 하고자 하는 것이 환경으로부터 값을 캡처할 필요가 없다면, Fn 트레이트 중 하나를 구현한 무언가가 필요한 곳에 클로저 대신 함수 이름을 사용할 수 있습니다.
  • 예를 들면 Option<Vec<T>>의 값 상에서 unwrap_or_else(Vec::new)를 호출하여 이 값이 None일 경우 비어있는 새 벡터를 얻을 수 있습니다.
  • 슬라이스 (예: &[i32])
    • 배열이나 벡터와 같은 집합 데이터의 일부분을 참조하는 타입
    • 슬라이스는 원본 데이터의 소유권을 가져오지 않고, 단지 그 데이터를 참조만 합니다.
    • 슬라이스는 &[T] 또는 &mut [T] 형태로 불변 또는 가변 참조로 표현됩니다.
  • "sort_by_key의 input인 클로저"의 트레이트 바운드는 왜 FnOnce 대신 FnMut인지를 알아봅시다.
    • 이 클로저는 처리하려는 슬라이스에서 현재 아이템에 대한 참조자(Rectangle)를 하나의 인수로 받아서, 순서를 매길 수 있는 K 타입의 값을 반환합니다.
    • 이 함수는 각 아이템의 특정 속성을 이용하여 슬라이스를 정렬하고 싶을 때 유용
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}
  • sort_by_key가 FnMut 클로저를 갖도록 정의된 이유는 이 함수가 클로저를 여러 번 호출하기 때문입니다: 슬라이스 내 각 아이템마다 한 번씩요.
    • 클로저 |r| r.width는 자신의 환경으로부터 어떤 것도 캡처나 변형, 혹은 이동을 시키지 않으므로, 트레이트 바운드 요건을 충족합니다.

  • 반면 아래 예시는 FnOnce 트레이트만 구현한 클로저의 예를 보여주는데, 이 클로저는 환경으로부터 값을 이동시키고 있습니다.
  • 컴파일러는 이 클로저를 sort_by_key에 사용할 수 없게 할 것입니다: (아래 코드는 에러남)
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("by key called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}
  • 따라서, 이 클로저는 오직 FnOnce만 구현하고 있습니다.
  • 이 코드를 컴파일 시도하면, 클로저가 FnMut를 구현해야 하기 때문에 value가 클로저 밖으로 이동될 수 없음을 지적하는 에러를 얻게 됩니다:
  • 위 코드 개선 방법:
    • sort_by_key가 호출되는 횟수를 세기 위해서는,
    • 환경 쪽에 카운터를 유지하면서, 클로저 본문에서 이 값을 증가시키는 것


2. 반복자로 일련의 아이템들 처리하기

  • 러스트에서의 반복자는 게으른데, 이는 반복자를 사용하는 메서드를 호출하여 반복자를 소비하기 전까지는 동작을 하지 않는다는 의미
  • 아래 코드 자체로는 어떤 유용한 동작도 하지 않습니다.
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
  • 반복자는 벡터처럼 인덱스를 사용할 수 있는 자료구조 뿐만 아니라, 많은 다른 종류의 시퀀스에 대해 동일한 로직을 사용할 수 있도록 더 많은 유연성을 제공

2.1. Iterator 트레이트와 next 메서드

  • 모든 반복자(위 v1_iter)는 표준 라이브러리에 정의된 Iterator라는 이름의 트레이트를 구현
  • 위 Item 타입이 next 메서드의 반환 타입으로 사용된다.
    • 바꿔 말하면, Item 타입은 반복자로부터 반환되는 타입
  • Iterator 트레이트는 구현하려는 이에게 딱 하나의 메서드 정의를 요구합니다:
    • 바로 next 메서드인데,
      • 이 메서드는 Some으로 감싼 반복자의 아이템을 하나씩 반환하고, 반복자가 종료될 때는 None을 반환
  • v1_iter를 가변으로 만들 필요가 있음을 주의하세요:
    • 반복자에 대한 next 메서드 호출: 반복자 내부의 상태를 변경하여 반복자가 현재 시퀀스의 어디에 있는지 추적
    • 바꿔 말하면, 이 코드는 반복자를 소비 (consume), 즉 다 써 버립니다.
  • for 루프를 사용할 때는 v1_iter를 가변으로 만들 필요가 없는데, 루프가 v1_iter의 소유권을 갖고 내부적으로 가변으로 만들기 때문
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {}", val);
    }
  • 또한 next 호출로 얻어온 값들은 벡터 내의 값들에 대한 불변 참조자라는 점도 주의
    • iter 메서드는 불변 참조자에 대한 반복자를 생성
    • v1의 소유권을 얻어서 소유한 값을 반환하도록 하고 싶다면, iter 대신 into_iter를 호출
    • 가변 참조자에 대한 반복자가 필요하면, iter 대신 iter_mut을 호출

2.2. 반복자를 소비하는 메서드

  • Iterator 트레이트에는 표준 라이브러리에서 기본 구현을 제공하는 여러 가지 메서드가 있음
    • 이 메서드들 중 일부는 정의 부분에서 next 메서드를 호출하는데, 이것이 Iterator 트레이트를 구현할 때 next 메서드를 구현해야만 하는 이유입니다.
  • next를 호출하는 메서드들을 소비 어댑터 (consuming adaptor) 라고 하는데, 호출하면 반복자를 소비하기 때문에 그렇습니다.
    • 한 가지 예로 sum 메서드가 있는데,
      • 이는 반복자의 소유권을 가져온 다음 반복적으로 next를 호출하는 방식으로 순회하며, 따라서 반복자를 소비
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }
  • sum은 반복자를 소유하여 호출하므로, sum을 호출한 이후에는 v1_iter의 사용이 허용되지 않습니다.

2.3. 다른 반복자를 생성하는 메서드

  • 아래는 반복자 어댑터 메서드인 map을 호출하는 예를 보여주는데, 클로저를 인수로 받아서 각 아이템에 대해 호출하여 아이템 전체를 순회합니다.
  • map 메서드는 수정된 아이템들을 생성하는 새로운 반복자를 반환합니다.
  • 여기에서의 클로저는 벡터의 각 아이템에서 1이 증가한 새로운 반복자(v1.iter().map(|x| x + 1)) 를 만듭니다:
  • 반복자(v1.iter().map(|x| x + 1))를 소비하기 위해서 collect 메서드를 사용할 것인데,
    • 이 collect 메서드는 반복자를 소비하고 결괏값을 모아서 컬렉션 데이터 타입으로 만들어 줍니다.
    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);

2.4. 환경을 캡처하는 클로저 사용하기

  • 많은 반복자 어댑터 메서드는 클로저를 인수로 사용하고,
    • 보통 반복자 어댑터 메서드의 인수에 명시되는 클로저는 자신의 환경을 캡처하는 클로저
  • 이러한 예를 들기 위해 클로저 인수를 사용하는 filter 메서드를 사용해 보겠습니다.
    • 이 클로저는 반복자로부터 아이템을 받아서 bool을 반환합니다.
    • 만일 클로저가 true를 반환하면, 그 값을 filter에 의해 생성된 반복자에 포함시키게 됩니다.
  • 아래 예제의 클로저는 환경에서 shoe_size 매개변수를 캡처하고 각 신발의 크기와 값을 비교하여 지정된 크기의 신발만 유지하도록 합니다.


3. I/O 프로젝트 개선하기

3.1. 반복자를 사용하여 clone 제거하기

  • CLI 인자를 받아 parsing하는 아래 코드
impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}
  • Config 인스턴스의 소유권을 반환하기 위해서는
    • Config의 query와 file_path 필드로 값을 복제하는 것으로 Config 인스턴스가 그 값들을 소유하게 할 필요가 있었습니다.
  • 반복자에 대한 새로운 지식을 사용하면,
    • 인수로써 슬라이스를 빌리는(args[1]) 대신 반복자의 소유권을 갖도록 build 함수를 변경할 수 있습니다.
    • "슬라이스의 길이를 체크하고 특정 위치로 인덱싱"하는 코드 대신, "반복자의 기능"을 사용
  • Config::build가 반복자의 소유권을 가져오고 -> 반복자의 String 값을 Config로 이동시킬 수 있습니다.

3.1.1. 반환된 반복자를 직접 사용하기

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --생략--
}
  • 위 코드를 아래와 같이 수정해서, Config::build 의 인자로 반복자를 넣자.
fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --생략--
}
  • env::args 함수는 반복자를 반환
  • env::args로부터 반환된 반복자의 소유권을 Config::build로 직접 전달

  • 이러한 impl Trait 문법을 사용하면
    • args가 Iterator 타입을 구현하면서 String 아이템을 반환하는 모든 종류의 타입을 사용할 수 있습니다.
  • args의 소유권을 가져와서 이를 순회하면서 args를 변경할 것이기 때문에,
    • args 매개변수의 명세 부분에 mut 키워드를 추가하여 가변이 되도록 합니다.

3.1.2. 인덱싱 대신 Iterator 트레이트 메서드 사용하기

impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

3.2. 반복자 어댑터 (메서드)로 더 간결한 코드 만들기

  • 아래는, contents 내에서 query가 들어간 문장을 찾아내는 코드. 리펙토링 대상
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}
  • 반복자 어댑터 메서드를 사용하면, 중간에 가변 results 벡터를 만들지 않아도 됩니다.
  • 함수형 프로그래밍 스타일은 (더 명확한 코드를 만들기 위해) 변경 가능한 상태의 양을 최소화하는 편을 선호합니다.
  • 가변 상태를 제거하면 results 벡터에 대한 동시 접근을 관리하지 않아도 되기 때문에,
    • 차후에 검색을 병렬로 수행하도록 하는 향상이 가능해집니다.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

3.3. 루프와 반복자 중 선택하기

  • 대부분의 러스트 프로그래머는 반복자 스타일을 선호합니다.
    • 처음 사용하기는 다소 어렵습니다만, 다양한 반복자 어댑터와 어떤 일을 하는지에 대해 일단 감을 잡으면 반복자들을 이해하기 쉬워질 것입니다.
  • 이는 몇몇 아주 흔한 코드를 추상화해서 제거하므로,
    • 반복자의 각 요소가 반드시 통과해야 하는 필터링 조건과 같이 이 코드에 유일한 개념을 더 알기 쉽게끔 합니다.


4. 성능 비교하기: 루프 vs. 반복자

  • 반복자는 비록 고수준의 추상화지만, 컴파일되면 대략 직접 작성한 저수준의 코드와 같은 코드 수준으로 내려갑니다.
  • 반복자는 러스트의 비용 없는 추상화 (zero-cost abstraction) 중 하나이며, 그 추상을 사용하는 것은 추가적인 런타임 오버헤드가 없다는 것을 의미
  • 러스트(Rust) 컴파일러에서 "루프 언롤링(unrolling)"은 반복문을 최적화하는 기술 중 하나
  • 이 기법은 컴파일러가 반복문을 변환하여 반복 횟수를 줄임으로써 성능을 개선하는 데 사용
  • 루프 언롤링의 기본 개념은 반복문 내에서 여러 번 수행해야 하는 작업을 한 번에 처리하는 것
  • 이를 통해 조건 검사와 같은 반복적인 작업의 오버헤드를 줄이고, 코드가 더 효율적으로 실행될 수 있습니다.

4.1. "루프 언롤링(unrolling)" 예제와 설명

  • 우선, 루프 언롤링이 없는 기본적인 루프 예제를 살펴봅시다.
fn sum_array(arr: &[i32]) -> i32 {
    let mut sum = 0;
    for i in 0..arr.len() {
        sum += arr[i];
    }
    sum
}

fn main() {
    let array = [1, 2, 3, 4, 5, 6, 7, 8];
    println!("Sum: {}", sum_array(&array));
}

이 코드는 배열의 요소를 순차적으로 더하는 단순한 루프입니다. for 반복문은 arr의 길이만큼 실행되며, 각 반복마다 배열의 요소를 더하게 됩니다.

4.1.1. 컴파일러가 "루프 언롤링(unrolling)"을 적용할 경우:

  • 루프 언롤링을 적용하면, 이 루프는 컴파일 과정에서 다음과 같이 변환될 수 있습니다.
fn sum_array(arr: &[i32]) -> i32 {
    let mut sum = 0;
    let len = arr.len();
    let mut i = 0;

    // 루프 언롤링을 적용한 부분
    while i + 3 < len {
        sum += arr[i] + arr[i + 1] + arr[i + 2] + arr[i + 3];
        i += 4;
    }

    // 남은 요소를 처리하는 루프
    while i < len {
        sum += arr[i];
        i += 1;
    }

    sum
}
  • 이 경우, while 루프는 한 번에 4개의 요소를 더하게 됩니다.
  • 이렇게 하면 각 반복마다 더 적은 수의 반복 조건 검사와 인덱싱이 필요하므로, 성능이 향상될 수 있습니다.
  • 루프 언롤링의 이점은 다음과 같습니다:
  1. 반복 조건 검사의 횟수 감소:
  • 반복문이 한 번 실행될 때마다 여러 요소를 처리하므로 반복 조건 검사의 횟수가 줄어듭니다.
  1. 캐시 효율성 향상:
  • 연속적인 메모리 접근이 더 효율적으로 이루어져 CPU 캐시의 히트율이 향상될 수 있습니다.
  1. 병렬 처리 최적화:
  • 컴파일러는 더 많은 데이터를 한 번에 처리할 수 있는 기회를 제공하므로, SIMD(Single Instruction, Multiple Data)와 같은 명령어 최적화에 유리할 수 있습니다.
  • 하지만, 루프 언롤링은 항상 최적의 선택은 아닙니다. 루프가 매우 길거나 데이터 양이 적다면 언롤링이 오히려 성능을 떨어뜨릴 수 있습니다.
  • 이 때문에 Rust 컴파일러는 루프 언롤링을 자동으로 적용할지 여부를 상황에 따라 판단합니다.
  • 이렇게 루프 언롤링이 성능 최적화에 어떻게 활용되는지 이해하면, 코드를 작성할 때 보다 성능을 고려한 코드를 설계하는 데 도움이 될 것입니다.
  • 클로저와 반복자의 구현은 런타임 성능에 영향을 미치지 않도록 설계 되었습니다. 이는 비용 없는 추상화를 제공하기 위해 노력하는 러스트의 목표 중 하나
  • registor: CPU 내부, 매우 빠른 메모리 공간


profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글