GCC가 std :: vector :: size가이 루프에서 변경되지 않는다고 가정 할 수없는 이유는 무엇입니까?


14

if (i < input.size() - 1) print(0);이 루프에서 최적화 input.size()되어 모든 반복에서 읽히지 않는 동료에게 주장 했지만 이것이 사실이 아님을 알 수 있습니다!

void print(int x) {
    std::cout << x << std::endl;
}

void print_list(const std::vector<int>& input) {
    int i = 0;
    for (size_t i = 0; i < input.size(); i++) {
        print(input[i]);
        if (i < input.size() - 1) print(0);
    }
}

gcc 옵션 이있는 컴파일러 탐색기 에 따르면 -O3 -fno-exceptions실제로 input.size()각 반복을 읽고 lea빼기를 수행하는 데 사용 됩니다!

        movq    0(%rbp), %rdx
        movq    8(%rbp), %rax
        subq    %rdx, %rax
        sarq    $2, %rax
        leaq    -1(%rax), %rcx
        cmpq    %rbx, %rcx
        ja      .L35
        addq    $1, %rbx

흥미롭게도 Rust에서이 최적화가 이루어집니다. 그것은과 같은 i변수로 대체됩니다 j각각의 반복을 감소하고, 시험은 i < input.size() - 1같은 대체됩니다 j > 0.

fn print(x: i32) {
    println!("{}", x);
}

pub fn print_list(xs: &Vec<i32>) {
    for (i, x) in xs.iter().enumerate() {
        print(*x);
        if i < xs.len() - 1 {
            print(0);
        }
    }
}

에서 컴파일러 탐색기 관련 어셈블리는 다음과 같습니다 :

        cmpq    %r12, %rbx
        jae     .LBB0_4

나는 확인하고 나는 확신 r12입니다 xs.len() - 1및 rbx카운터입니다. 이전에는 addfor rbx및 mov외부 루프가 r12있습니다.

왜 이런거야? GCC가 인라인 할 수 size()있고 operator[]마치 size()변경되지 않는 것을 알 수 있어야합니다 . 그러나 GCC의 옵티마이 저는 변수로 가져올 가치가 없다고 판단합니까? 또는 이로 인해 안전하지 않은 다른 부작용이있을 수 있습니다.


1
또한 println복잡한 방법 일 수도 있습니다. 컴파일러는 println벡터를 변경하지 않는지 입증하는 데 문제가있을 수 있습니다 .
— Mooing Duck

1
@MooingDuck : 또 다른 스레드는 데이터 레이스 UB입니다. 컴파일러는 수와 가정 않는 이 발생하지 않습니다. 여기서 문제는에 대한 비 인라인 함수 호출 cout.operator<<()입니다. 컴파일러는이 블랙 박스 함수가 std::vector전역에서 참조를 얻지 못한다는 것을 모른다 .
— Peter Cordes

@PeterCordes : 다른 스레드는 독립형 설명이 아니며 복잡 println하거나 operator<<핵심입니다.
— Mooing Duck

컴파일러는 이러한 외부 메소드의 의미를 모릅니다.
— user207421

답변:


10

인라인이 아닌 함수 호출 cout.operator<<(int)은 옵티마이 저의 블랙 박스입니다 (라이브러리는 C ++로 작성되었으며 모든 옵티마이 저가 프로토 타입이므로 주석의 설명 참조). 전역 변수가 가리킬 수있는 메모리가 수정되었다고 가정해야합니다.

(또는 std::endl전화. BTW, 왜 그냥 인쇄하지 않고 그 시점에서 cout을 강제로 플러시 '\n'합니까?)

예 를std::vector<int> &input 들어 모든 변수에 대한 참조는 전역 변수에 대한 참조이며 해당 함수 호출 중 하나는 해당 전역 변수를 수정합니다 . 또는 vector<int> *ptr어딘가에 전역이 있거나 static vector<int>다른 컴파일 단위 의 포인터를 반환하는 함수가 있거나 함수가이 벡터에 대한 참조를 전달하지 않고이 벡터에 대한 참조를 얻을 수있는 다른 방법이 있습니다.

주소를 가져 오지 않은 로컬 변수가있는 경우 컴파일러 는 인라인이 아닌 함수 호출이이를 변경할 수 없다고 가정 할 수 있습니다. 전역 변수 가이 객체에 대한 포인터를 가질 수있는 방법이 없기 때문입니다. ( 이것은이라고 분석 탈출 ). 이것이 컴파일러가 size_t i함수 호출에서 레지스터를 유지할 수있는 이유 입니다. ( int i어두워지고 size_t i달리 사용되지 않기 때문에 최적화 할 수 있습니다 ).

로컬 vector(예 : base, end_size 및 end_capacity 포인터)과 동일한 작업을 수행 할 수 있습니다.

ISO C99에는이 문제에 대한 해결책이 있습니다 int *restrict foo. 많은 C ++ 컴파일 지원 int *__restrict foo메모리가 가리키는 것을 약속 foo한다 에만 해당 포인터를 통해 액세스. 2 개의 배열을 취하는 함수에 가장 일반적으로 유용하며 컴파일러가 겹치지 않도록 약속하고 싶습니다. 따라서 코드를 생성하지 않고 자동 벡터화하여이를 확인하고 폴백 루프를 실행할 수 있습니다.

OP 의견 :

Rust에서 변경할 수없는 참조는 다른 사람이 당신이 참조 한 값을 변경하지 않는다는 것을 보장합니다 (C ++과 동일 restrict)

그것은 Rust가 왜이 최적화를 할 수 있지만 C ++는 할 수 없는지를 설명합니다.


C ++ 최적화

분명히 auto size = input.size();함수 상단에서 한 번만 사용해야 컴파일러가 루프 불변임을 알 수 있습니다. C ++ 구현은이 문제를 해결하지 못하므로 직접해야합니다.

또한 "제어 블록" const int *data = input.data();에서 데이터 포인터의로드를 들어 올려야 할 수도 std::vector<int>있습니다. 불행히도 최적화에는 매우 비 이념적 인 소스 변경이 필요할 수 있습니다.

Rust는 훨씬 더 현대적인 언어로, 컴파일러 개발자가 컴파일러에서 실제로 가능한 것을 알게 된 후에 고안되었습니다. 이식 재료의 CPU를 통해 할 수있는 멋진 일부 노출을 포함하여 너무 다른 방법에 그것은 정말 쇼 i32.count_ones를 제외한 회전, 비트 스캔 등 그것은 ISO C ++에서 여전히 이식이 중 하나를 노출하지 않습니다 정말 멍청한를 std::bitset::count().


1
벡터가 값으로 취해지는 경우 OP의 코드는 여전히 테스트를 수행합니다. 따라서 GCC가이 경우 최적화 할 수는 있지만 그렇게하지는 않습니다.
— 호두

1
표준은 operator<<이러한 피연산자 유형 의 동작을 정의합니다 . 따라서 표준 C ++에서는 블랙 박스가 아니며 컴파일러는 설명서의 내용을 수행한다고 가정 할 수 있습니다. 아마도 그들은 비표준 행동을 추가하는 라이브러리 개발자를 지원하고 싶을 것입니다.
— MM

2
최적화는 표준 의무가 내 지점이 최적화이 최적화 당신이 설명하는 방법으로 구현하고 지내다 표준하지만 컴파일러 공급 업체 선택한다면 허용되는 것을하는 동작 공급 될 수있다
— MM

2
@MM 그것은 임의의 객체를 말하지 않았고 구현 정의 벡터를 말했다. 표준에는 구현자가 구현 정의 방식으로이 벡터에 액세스하고 액세스하도록 허용하는 구현 정의 벡터가 구현을 금지하는 것은 없습니다. cout를 사용하여 파생 된 사용자 정의 클래스의 객체를 streambuf스트림과 연결할 수 있습니다 cout.rdbuf. 이와 유사하게 파생 된 객체를에 ostream연결할 수 있습니다 cout.tie.
— 로스 릿지

2
@PeterCordes-로컬 벡터에 대해 너무 확신하지 못합니다. 멤버 함수가 아웃 라인되면 this포인터가 암시 적으로 전달 되므로 로컬이 효과적으로 탈출했습니다 . 이것은 실제로 생성자만큼 일찍 발생할 수 있습니다. 고려 이 간단한 루프 - 난 단지 (에서 GCC 메인 루프를 확인 L34:하는 jne L34),하지만 확실히 벡터 구성원이 탈출 한 것처럼 (메모리에서 각각의 반복 그들을로드) 행동한다.
— BeeOnRope
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.