2016-11-05 1 views
7

컴파일러에서 iter().map().sum()iter().fold()에 대해 동일한 코드를 생성합니까? 결국 같은 목표를 달성하지만 첫 코드는 map에 대해 한 번, sum에 대해 두 번 반복됩니다.iter(). map(). sum()`iter(). fold()`만큼 빠름?

다음은 예입니다. 어느 버전이 total에서 빠를까요?

pub fn square(s: u32) -> u64 { 
    match s { 
     s @ 1...64 => 2u64.pow(s - 1), 
     _ => panic!("Square must be between 1 and 64") 
    } 
} 

pub fn total() -> u64 { 
    // A fold 
    (0..64).fold(0u64, |r, s| r + square(s + 1)) 
    // or a map 
    (1..64).map(square).sum() 
} 

조립품을 보거나이를 벤치마킹하는 데 유용한 도구는 무엇입니까?

+2

잠깐, 반복한다는 말은 무엇입니까? * 두 번 반복 하시겠습니까? –

+1

@MatthieuM. 일반적인 오해. 예를 들어, Ruby와 같은 언어는 '지도'의 결과로 전체 '배열'을 생성합니다. 따라서 각각의 연결된'map' 호출은 새로운 컨테이너를 반복해야합니다. 루비에는 "게으른"반복자가 있지만 일반적이지 않습니다. – Shepmaster

+1

@Shepmaster : 그건 내가 두려워하는 것입니다. –

답변

14

동일한 코드를 생성하려면 먼저 과 동일한 코드를 사용해야합니다.. 당신의 두 가지 예는하지 않습니다 :

  1. 생성 된 LLVM IR
  2. 을 :

    fn total_fold() -> u64 { 
        (0..64).fold(0u64, |r, s| r + square(s + 1)) 
    } 
    
    fn total_map() -> u64 { 
        (1..64).map(square).sum() 
    } 
    
    fn main() { 
        println!("{}", total_fold()); 
        println!("{}", total_map()); 
    } 
    
    18446744073709551615 
    9223372036854775807 
    

    이의 당신이

    fn total_fold() -> u64 { 
        (1..64).fold(0u64, |r, s| r + square(s + 1)) 
    } 
    
    fn total_map() -> u64 { 
        (1..64).map(|i| square(i + 1)).sum() 
    } 
    

    확인하는 데 몇 도로가 있습니다 의미 가정 해 봅시다

  3. 생성 된 a ssembly
  4. 벤치

적외선 및 조립의 쉬운 소스 운동장 (official 또는 alternate) 중 하나이다. 이 두 가지 모두 어셈블리 또는 IR을 볼 수있는 버튼이 있습니다. --emit=llvm-ir 또는 --emit=asm을 컴파일러에 전달하여 이러한 파일을 생성 할 수도 있습니다.

릴리스 모드에서 어셈블리 또는 IR을 생성해야합니다. 속성 #[inline(never)]은 출력에서 ​​더 쉽게 찾을 수 있도록 기능을 분리하여 유지하는 데 종종 유용합니다.

벤치마킹은 documented in The Rust Programming Language이므로 중요한 정보를 모두 반복 할 필요가 없습니다.


녹 1.14하기 전에 다음은 정확한 동일한 어셈블리를 생성하지 않습니다. 벤치마킹/프로파일 링 데이터를 기다려서 걱정하기 전에 성능에 의미있는 영향이 있는지 확인합니다.

녹슬림 1.14의 경우 은 동일한 조립품을 생산합니까! 이것이 내가 녹을 사랑하는 이유 중 하나입니다. 명확하고 관용적 인 코드와 smart people come along and make it equally as fast을 작성할 수 있습니다.

그러나 첫 번째 코드는 map에 대해 한 번, sum에 대해 두 번 반복됩니다.

이것은 잘못된 것이며, 어떤 소스에서 알려 주 었는지 알고 싶습니다. 그 시점에서 올바른 정보를 찾아 미래의 오해를 예방할 수 있습니다. An iterator은 풀 기반으로 작동합니다. 하나의 요소가 한 번에 처리됩니다.핵심 방법은 next이며 단일 값을 산출하고 해당 값을 생성하기에 충분한 계산을 실행합니다.

5

먼저, 실제로 같은 결과를 반환하는 예를 해결하자

이제
pub fn total_fold_iter() -> u64 { 
    (1..65).fold(0u64, |r, s| r + square(s)) 
} 

pub fn total_map_iter() -> u64 { 
    (1..65).map(square).sum() 
} 

,의는 fold부터 시작을 개발 할 수 있습니다. ,의는 mapsum 함께 가자, 및 상당 인의 sum 최초의 포장을 푸는 그런

pub fn total_fold_explicit() -> u64 { 
    let mut total = 0; 
    for i in 1..65 { 
     total = total + square(i); 
    } 
    total 
} 

:

fold는 대략에 해당
, 그냥 루프와 축적이다
pub fn total_map_partial_iter() -> u64 { 
    let mut total = 0; 
    for i in (1..65).map(square) { 
     total += i; 
    } 
    total 
} 

그냥 간단한 축전기입니다! 이들의 모두 매우 비슷

pub fn total_map_explicit() -> u64 { 
    let mut total = 0; 
    for i in 1..65 { 
     let s = square(i); 
     total += s; 
    } 
    total 
} 

당신이 볼 수 있듯이 : 그리고 지금, 대략 상당에 무언가를 얻기, 이제 (단지 함수를 적용)를 map 계층 랩을 해제하자 그들이 가지고 동일한 순서로 같은 작업을 적용하고 전체적으로 동일한 복잡성을 갖습니다.


어느 쪽이 빠릅니까? 나는 모른다. 그리고 마이크로 벤치 마크는 어쨌든 진실의 절반만을 말해 줄 수 있습니다. 마이크로 벤치 마크에서 어떤 것이 더 빠르다고해서 다른 코드의 속에서 더 빠르다는 것을 의미하지는 않습니다.

그러나 I 입니다. 그러나 둘 다 동일한 복잡성을 가지므로 서로 비슷하게 동작해야합니다.

그리고 더 명확 foldIterator 방법 "부엌 싱크대"따라서 훨씬 덜 유익한 반면 의도를 표현하기 때문에 개인적으로, map + sum 갈 것이라고

.

+1

"일반"for 루프를 얻으려는 변환은 컴파일러가 작동하는 방식이 아닙니다. 아마도 많은 최적화 작업을 거친 후에 비슷한 결과를 얻게 될 것입니다. (사실 두 개의 반복기 체인이 동일한 기계어 코드를 사용한다는 것은 의심의 여지가 없습니다.) 컴파일러가 두 개의 반복기 체인을 제외시키는 것은 아닙니다. 대조적으로, 당신이 끝내는 루프는 문자 그대로 동일한 코드를 생성해야하므로 매우 똑같습니다. — 차이점은 컴파일러에게도 완전히 미적입니다. – delnan

+2

나는이 시나리오에 아무런 영향을 미치지 않는다고 생각합니다. (누가 알겠습니까?)하지만,'.map() '은 그냥 폴드하는 법을 배웠습니다. 그래서 이제 그 부분을 명시 적으로 재 작성 중입니다. https://github.com을보십시오./rust-lang/rust/pull/37315 – bluss

+1

@delnan 아주 작은 사소한 뾰족 함을 제외하고 컴파일러가 인라인 처리 한 후에 보는 것과 거의 같습니다. 당신은 어느 시점에서 동의하지 않습니까? – Veedrac

관련 문제