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() - 1rbx카운터입니다. 이전에는 addfor rbxmov외부 루프가 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.