const가 조기 최적화를 참조 할 때 인수를 전달합니까?


20

"조기 최적화는 모든 악의 근원"

우리 모두 동의 할 수 있다고 생각합니다. 그리고 나는 그것을 피하기 위해 매우 열심히 노력합니다.

그러나 최근에는 Value 대신 const Reference로 매개 변수를 전달하는 방법에 대해 궁금 합니다 . 나는 사소하지 않은 함수 인수 (즉, 대부분의 기본이 아닌 유형)는 const 참조 로 전달하는 것이 바람직하다는 것을 배웠습니다. 필자는 읽은 책 중 몇 권을 "모범 사례"로 권장합니다.

여전히 도움이 될 수는 없습니다. 현대의 컴파일러와 새로운 언어 기능은 놀라운 일이 될 수 있습니다. 따라서 배운 지식은 매우 오래되었을 수 있으며 성능 차이가있을 경우 실제로 프로파일 링을하지 않습니다.

void fooByValue(SomeDataStruct data);   

void fooByReference(const SomeDataStruct& data);

내가 배운 관행 -const 참조를 전달하는 것이 (사소하지 않은 유형의 경우 기본적으로) 조기 최적화입니까?


1
다양한 매개 변수 전달 전략에 대한 설명은 C ++ 핵심 지침의 F.call을 참조하십시오 .
amon


1
@DocBrown이 질문에 대한 답은 여기에 적용될 수 있는 가장 놀라운 원리를 나타냅니다 (즉, const 참조를 사용하는 것은 산업 표준 등입니다). 즉, 나는 질문이 중복이라는 것에 동의하지 않습니다. 당신이 말하는 질문은 (일반적으로) 컴파일러 최적화에 의존하는 것이 나쁜 습관인지 묻습니다. 이 질문은 그 반대입니다 : const 참조를 전달하는 것이 (조기) 최적화입니까?
CharonX

@CharonX : 여기 컴파일러 최적화에 의존 할 수 있다면 귀하의 질문에 대한 대답은 분명히 "예, 수동 최적화가 필요하지 않으며 조기입니다"입니다. 코드에 의존 할 수없는 경우 (어떤 컴파일러가 코드에 사용될 것인지 미리 알지 못하기 때문에) "대형 객체의 경우 아마 이른 것은 아닙니다"입니다. 따라서 두 질문이 문자 적으로 동일하지 않더라도 IMHO는 질문을 중복으로 연결하기에 충분할 것 같습니다.
Doc Brown

1
@DocBrown : 따라서 속임수를 선언하기 전에 컴파일러에서 허용하고이를 "최적화"할 수 있다고 말하는 곳을 지적하십시오.
중복 제거기

답변:


49

"조기 최적화"는 최적화를 조기 에 사용하는 것이 아닙니다 . 문제를 이해하기 전에, 런타임을 이해하기 전에 최적화하고, 모호한 결과에 대해 코드를 읽기 어렵고 유지 관리하기 어려워하는 경우가 많습니다.

객체를 값으로 전달하는 대신 "const &"를 사용하는 것은 런타임에 잘 이해되고 실제로 노력하지 않고 가독성 및 유지 관리성에 나쁜 영향을주지 않는 잘 이해 된 최적화입니다. 호출은 전달 된 객체를 수정하지 않는다고 알려주기 때문에 실제로 두 가지를 모두 향상시킵니다. 따라서 코드를 작성할 때 "const &"를 추가하는 것은 PREMATURE가 아닙니다.


2
귀하의 답변에 "실제로 노력하지 않는"부분에 동의합니다. 그러나 조기 최적화는 주목할만한 측정 성능에 영향을 미치기 전에 최적화에 관한 것입니다. 그리고 나는 대부분의 C ++ 프로그래머 (자신을 포함하는)가을 사용하기 전에 측정을한다고 생각하지 않으므로 const&질문이 꽤 합리적이라고 생각합니다.
Doc Brown

1
트레이드 오프가 가치가 있는지 여부를 알기 위해 최적화하기 전에 측정합니다. const &를 사용하면 총 노력 7 문자를 입력하고 다른 장점이 있습니다. 전달되는 변수를 수정하지 않으려는 경우 속도 향상이 없어도 유리합니다.
gnasher729 2016 년

3
나는 C 전문가가 아니므로 질문 :. const& foo함수가 foo를 수정하지 않으므로 호출자 가 안전합니다. 그러나 복사 된 값은 다른 스레드 가 foo를 변경할 수 없으므로 호출 수신자 가 안전하다는 것을 나타냅니다. 권리? 따라서 멀티 스레드 응용 프로그램에서 대답은 최적화가 아닌 정확성에 달려 있습니다.
user949300

1
@DocBrown 당신은 결국 const &?를 넣은 개발자의 동기를 물을 수 있습니다. 나머지를 고려하지 않고 성능을 위해서만 수행 한 경우 조기 최적화로 간주 될 수 있습니다. 이제 그가 const 매개 변수가 될 것이라는 것을 알고 있기 때문에 코드를 작성하면 코드를 자체 문서화하고 컴파일러가 최적화 할 수있는 기회를 제공하는 것이 좋습니다.
Walfrat

1
@ user949300 : 함수를 사용하여 동시에 또는 콜백을 통해 인수를 수정할 수있는 함수는 거의 없으며 명시 적으로 말합니다.
중복 제거기

16

TL; DR : const 참조를 통한 전달은 C ++에서 여전히 좋은 생각입니다. 조기 최적화가 아닙니다.

TL; DR2 : 대부분의 격언은 그럴 때까지 말이되지 않습니다.


목표

이 답변은 C ++ 핵심 지침 (amon의 의견에서 처음 언급 됨)에서 연결된 항목을 약간 확장하려고합니다 .

이 답변은 프로그래머의 영역 내에서 널리 퍼진 다양한 격언, 특히 상충되는 결론 또는 증거 사이의 화해 문제를 올바르게 생각하고 적용하는 방법에 대한 문제를 다루려고하지 않습니다.


적용 성

이 답변은 함수 호출 (같은 스레드에서 분리 할 수없는 중첩 범위)에만 적용됩니다.

(측면 참고) 통과 가능한 항목이 범위를 벗어날 수있는 경우 (즉, 수명이 외부 범위를 초과 할 수있는 경우) 다른 것보다 먼저 응용 프로그램의 객체 수명 관리 요구를 충족시키는 것이 중요합니다. 일반적으로 스마트 포인터와 같이 수명 관리가 가능한 참조를 사용해야합니다. 대안은 관리자를 사용하는 것일 수 있습니다. 람다는 분리 가능한 범위의 일종입니다. 람다 캡처는 객체 범위를 갖는 것처럼 동작합니다. 따라서 람다 캡처에주의하십시오. 또한 람다 자체가 복사 또는 참조로 전달되는 방식에주의하십시오.


가치를 지날 때

변경 가능 통신 (공유 참조)이 필요하지 않은 스칼라 값 (머신 레지스터 내에 적합하고 의미 론적 값을 갖는 표준 프리미티브)의 경우 값을 전달하십시오.

수신자가 객체 또는 집계의 복제를 요구하는 경우, 수신자의 사본이 복제 된 객체의 필요성을 충족시키는 값으로 전달합니다.


참조로 전달할 때 등

다른 모든 상황에서는 포인터, 참조, 스마트 포인터, 핸들 (핸들-바디 관용구 등)을 사용하십시오.이 조언을 따를 때마다 평소대로 const-correctness의 원칙을 적용하십시오.

메모리 공간이 충분히 큰 사물 (집계, 객체, 배열, 데이터 구조)은 성능상의 이유로 항상 참조를 통한 전달을 용이하게하도록 설계되어야합니다. 이 조언은 수백 바이트 이상일 때 확실히 적용됩니다. 이 조언은 수십 바이트 일 때 경계선입니다.


특이한 패러다임

의도적으로 복사가 많은 특수 목적의 프로그래밍 패러다임이 있습니다. 예를 들어, 문자열 처리, 직렬화, 네트워크 통신, 격리, 써드 파티 라이브러리 랩핑, 공유 메모리 프로세스 간 통신 등. 이러한 응용 프로그램 영역 또는 프로그래밍 패러다임에서 데이터는 구조체에서 구조체로 복사되거나 때로는 다시 패키징됩니다. 바이트 배열.


최적화가 고려 되기 전에 언어 사양이이 답변에 미치는 영향

Sub-TL; DR 참조 전파는 코드를 호출하지 않아야합니다. const-reference로 전달하면이 기준을 충족합니다. 그러나 다른 모든 언어는이 기준을 손쉽게 충족시킵니다.

초보자 C ++ 프로그래머는이 섹션을 완전히 건너 뛰는 것이 좋습니다.

(이 섹션의 시작 부분은 gnasher729의 답변에서 부분적으로 영감을 얻었지만 다른 결론에 도달했습니다.)

C ++는 사용자 정의 복사 생성자와 할당 연산자를 허용합니다.

(이것은 놀랍고 유감스러운 대담한 선택이었습니다. 오늘날 언어 디자인에서 허용되는 표준과의 차이점입니다.)

C ++ 프로그래머가 정의하지 않더라도 C ++ 컴파일러는 언어 원칙에 따라 이러한 메소드를 생성 한 다음 이외의 코드를 추가로 실행해야하는지 여부를 결정해야합니다 memcpy. 예를 들어,가 class/ struct이은을 포함std::vector 멤버 사본 생성자와 사소하지 않은 할당 연산자가 있어야합니다.

다른 언어에서는 언어 설계에 따라 객체에 참조 의미가 있기 때문에 복사 생성자와 객체 복제는 권장되지 않습니다 (응용 프로그램의 의미에 절대적으로 필요하거나 의미있는 경우 제외). 이러한 언어에는 일반적으로 범위 기반 소유권 또는 참조 계산 대신 도달 가능성을 기반으로하는 가비지 수집 메커니즘이 있습니다.

C ++ (또는 C)에서 참조 또는 포인터 (const 참조 포함)가 전달되면 프로그래머는 주소 값의 전파 외에는 특별한 코드 (사용자 정의 또는 컴파일러 생성 함수)가 실행되지 않습니다. (참조 또는 포인터). 이것은 C ++ 프로그래머들이 익숙한 행동의 명확성입니다.

그러나 배경은 C ++ 언어가 불필요하게 복잡하여 이러한 행동의 명확성이 핵 낙진 지역 어딘가에있는 오아시스 (생존 가능한 서식지)와 같다는 것입니다.

더 많은 축복 (또는 모욕)을 추가하기 위해 C ++은 사용자 정의 이동 연산자 (이동 생성자 및 이동 할당 연산자)를 쉽게 수행 할 수 있도록 범용 참조 (r- 값)를 도입합니다. 이는 복사 및 딥 클로닝의 필요성을 줄임으로써 관련성이 높은 사용 사례 (한 인스턴스에서 다른 인스턴스로 오브젝트를 이동 (전송))하는 데 도움이됩니다. 그러나 다른 언어에서는 그러한 물체의 이동에 대해 말하는 것이 비논리적입니다.


(주제 이외의 섹션) "Want Speed? Pass by Value!"기사 전용 섹션입니다. 2009 년경.

이 기사는 2009 년에 작성되었으며 C ++의 r- 값에 대한 설계 정당성을 설명합니다. 이 기사는 이전 섹션의 결론에 대한 유효한 반론을 제시합니다. 그러나 기사의 코드 예제와 성능 주장은 오랫동안 반박되었습니다.

Sub-TL; DR C ++에서 r- 값 시맨틱의 설계는 놀랍도록 우아한 사용자 측 시맨틱을 허용합니다.Sort 예를 들어 함수에서 . 이 우아함은 다른 언어로 모델링 (모방) 할 수 없습니다.

정렬 함수는 전체 데이터 구조에 적용됩니다. 위에서 언급했듯이 많은 복사가 포함되면 속도가 느려집니다. 성능 최적화 (실제로 관련성이 있음)로서 정렬 함수는 C ++ 이외의 다른 언어에서는 파괴적으로 설계되었습니다. 파괴는 목표 데이터 구조가 정렬 목표를 달성하도록 수정되었음을 의미합니다.

C ++에서 사용자는 두 가지 구현 중 하나를 선택할 수 있습니다. 더 나은 성능을 가진 파괴적인 것 또는 입력을 수정하지 않는 일반적인 것입니다. 간결성을 위해 템플릿은 생략되었습니다.

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

정렬 이외에도이 우아함은 재귀 적 분할에 의해 배열 (처음 정렬되지 않은)에서 파괴적인 중앙값 찾기 알고리즘을 구현하는 데 유용합니다.

그러나 대부분의 언어는 배열에 파괴적인 정렬 알고리즘을 적용하는 대신 균형 잡힌 이진 검색 트리 접근 방식을 정렬에 적용합니다. 따라서이 기술의 실제 관련성은 그다지 높지 않습니다.


컴파일러 최적화가이 답변에 미치는 영향

인라인 (및 전체 프로그램 최적화 / 링크 타임 최적화)이 여러 수준의 함수 호출에 적용되면 컴파일러는 데이터 흐름을 볼 수 있습니다 (때로는 철저하게). 이 경우 컴파일러는 많은 최적화를 적용 할 수 있으며 그 중 일부는 메모리에 전체 객체를 생성하지 않아도됩니다. 일반적으로이 상황이 적용되는 경우 컴파일러가 철저하게 분석 할 수 있으므로 매개 변수가 값 또는 const-reference로 전달되는지는 중요하지 않습니다.

그러나 하위 수준 함수가 분석 이외의 것을 호출하는 경우 (예 : 컴파일 외부의 다른 라이브러리 또는 너무 복잡한 호출 그래프) 컴파일러는 방어 적으로 최적화해야합니다.

머신 레지스터 값보다 큰 객체는 명시 적 메모리로드 / 저장 명령어 또는 적절한 memcpy기능 호출에 의해 복사 될 수 있습니다 . 일부 플랫폼에서 컴파일러는 두 개의 메모리 위치 사이를 이동하기 위해 SIMD 명령어를 생성하며 각 명령어는 수십 바이트 (16 또는 32) 씩 이동합니다.


상세 또는 시각적 혼란 문제에 대한 토론

C ++ 프로그래머는 이것에 익숙합니다. 즉, 프로그래머가 C ++를 싫어하지 않는 한 소스 코드에서 const-reference를 작성하거나 읽는 오버 헤드는 끔찍하지 않습니다.

비용-편익 분석은 여러 번 수행되었을 수 있습니다. 인용해야 할 과학적인 것들이 있는지 모르겠습니다. 대부분의 분석은 과학적이지 않거나 재현 할 수없는 것 같습니다.

여기에 내가 상상 한 것이 있습니다 (증거 또는 신뢰할만한 참조가없는) ...

  • 예,이 언어로 작성된 소프트웨어의 성능에 영향을줍니다.
  • 컴파일러가 코드의 목적을 이해할 수 있다면 잠재적으로 자동화 할 수있을 정도로 똑똑 할 수 있습니다
  • 불행히도, 가변성을 선호하는 언어 (기능적 순도와 반대되는 언어)에서 컴파일러는 대부분의 것들이 변형 된 것으로 분류하므로, constness의 자동 추론은 대부분의 것들을 불변으로 거부합니다
  • 정신적 오버 헤드는 사람들에 달려 있습니다. 이것을 높은 정신적 오버 헤드로 생각하는 사람들은 C ++을 실행 가능한 프로그래밍 언어로 거부했을 것입니다.

이것은 하나를 선택하는 대신 두 가지 답변을 받아 들일 수 있기를 바라는 상황 중 하나입니다 ... sigh
CharonX

8

그는 DonaldKnuth의 논문 "StructuredProgrammingWithGoToStatements"에서 다음과 같이 썼습니다. 우리는 시간의 97 % 정도의 작은 효율성에 대해 잊어야합니다. 조기 최적화는 모든 악의 근원입니다. 그러나 중요한 3 %의 기회를 포기해서는 안됩니다. " -조기 최적화

이것은 프로그래머에게 가능한 가장 느린 기술을 사용하도록 조언하지 않습니다. 프로그램을 작성할 때 명확성 에 중점을 둡니다 . 명확성과 효율성이 절충되는 경우가 종종 있습니다. 하나만 선택해야하는 경우 명확성을 선택하십시오. 그러나 두 가지를 모두 쉽게 달성 할 수 있다면 효율성을 피하기 위해 명확함 (무엇이 일정하다는 신호를 보내는 등)을 허비 할 필요가 없습니다.


3
"하나만 골라야한다면 명료성을 선택하십시오." 두 번째는 다른 것을 선택해야 할 수도 있으므로 대신 선호 해야합니다 .
중복 제거기

@Deduplicator 감사합니다. 그러나 OP의 맥락에서 프로그래머는 자유롭게 선택할 수 있습니다.
Lawrence

당신의 대답은 그것보다 조금 더 일반적으로 읽습니다.
Deduplicator

@ 중복 제거기 아,하지만 내 대답의 맥락은 (또한) 프로그래머 가 선택하는 것입니다. 선택이 프로그래머에게 강요 되었다면, 선택을하는 것은 "당신"이 아닙니다 :). 나는 당신이 제안한 변경 사항을 고려했으며 그에 따라 답변을 편집하는 데 반대하지는 않지만 명확성을 위해 기존 문구를 선호합니다.
로렌스

7

([const] [rvalue] reference) | (value)에 의한 전달은 인터페이스의 의도와 약속에 관한 것이어야합니다. 성능과 관련이 없습니다.

리치의 엄지 손가락 규칙 :

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state

3

이론적으로 대답은 '예'여야합니다. 그리고 사실, 그것은 시간의 일부입니다. 사실, 값을 전달하는 대신 const 참조를 전달하면 전달 된 값이 너무 커서 단일 값에 맞지 않는 경우에도 비관이 될 수 있습니다 등록 (또는 사람들이 가치를 지날 때를 결정하기 위해 사용하는 다른 휴리스틱의 대부분). 몇 년 전 David Abrahams는 "Want Speed? Pass by Value!"라는 기사를 썼습니다. 이러한 경우 중 일부를 다루고 있습니다. 더 이상 찾기가 쉽지 않지만 사본을 파낼 수 있다면 읽을 가치가 있습니다 (IMO).

그러나 const 참조를 전달하는 특정 경우에는 관용구가 너무 잘 확립되어 상황이 다소 역전된다고 말할 수 있습니다 . 유형이 char/ short/ int/long 사람들은 const가 기본적으로 참조되므로 다른 특별한 이유가 없다면 그와 함께하는 것이 가장 좋습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.