우리는 질문을 한 사이의 성능 차이가 i++
와 ++i
C의는 ?
C ++에 대한 답은 무엇입니까?
우리는 질문을 한 사이의 성능 차이가 i++
와 ++i
C의는 ?
C ++에 대한 답은 무엇입니까?
답변:
[실행 요약 : 사용할 ++i
특별한 이유가없는 경우 사용 i++
]
C ++의 경우 대답이 조금 더 복잡합니다.
경우 i
, 간단한 유형 (안 C ++ 클래스의 인스턴스)입니다 다음 C에 주어진 답은 ( "아니 성능에 차이가") 컴파일러가 코드를 생성하기 때문에, 보유하지 않습니다.
그러나, 경우는 i
C ++ 클래스의 인스턴스는, 인 i++
및 ++i
중 하나를 호출하고 있습니다 operator++
기능. 이러한 기능의 표준 쌍은 다음과 같습니다.
Foo& Foo::operator++() // called for ++i
{
this->data += 1;
return *this;
}
Foo Foo::operator++(int ignored_dummy_value) // called for i++
{
Foo tmp(*this); // variable "tmp" cannot be optimized away by the compiler
++(*this);
return tmp;
}
컴파일러는 코드를 생성하지 않고 operator++
함수를 호출하기 때문에 tmp
변수와 관련 복사 생성자 를 최적화 할 수있는 방법이 없습니다 . 복사 생성자가 비싸면 성능에 상당한 영향을 줄 수 있습니다.
예. 있습니다.
++ 연산자는 함수로 정의되거나 정의되지 않을 수 있습니다. 기본 유형 (int, double, ...)의 경우 연산자가 내장되어 있으므로 컴파일러가 코드를 최적화 할 수 있습니다. 그러나 ++ 연산자를 정의하는 객체의 경우 상황이 다릅니다.
operator ++ (int) 함수는 사본을 작성해야합니다. postfix ++는 보유하고있는 것과 다른 값을 반환해야하기 때문입니다. 임시 변수에 값을 보유하고 값을 증가시킨 다음 온도를 반환해야합니다. 접두사 ++ 인 operator ++ ()의 경우 복사본을 만들 필요가 없습니다. 개체는 자체적으로 증가한 다음 간단히 반환 할 수 있습니다.
다음은 요점을 보여줍니다.
struct C
{
C& operator++(); // prefix
C operator++(int); // postfix
private:
int i_;
};
C& C::operator++()
{
++i_;
return *this; // self, no copy created
}
C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t; // return a copy
}
operator ++ (int)를 호출 할 때마다 사본을 작성해야하며 컴파일러는 이에 대해 아무것도 수행 할 수 없습니다. 선택권이 주어지면 operator ++ (); 이 방법으로 사본을 저장하지 않습니다. 많은 증분 (큰 루프?) 및 / 또는 큰 객체의 경우 중요 할 수 있습니다.
C t(*this); ++(*this); return t;
두 번째 줄에서는이 포인터를 오른쪽으로 늘리는 것이므로이를 늘리면 어떻게 t
업데이트됩니까? 이것의 값이 이미 복사되지 않았습니까 t
?
The operator++(int) function must create a copy.
전혀 그렇지 않다. 더 이상 사본operator++()
증분 연산자가 다른 변환 단위에있는 경우에 대한 벤치 마크입니다. g ++ 4.5가 포함 된 컴파일러
지금은 스타일 문제를 무시하십시오.
// a.cc
#include <ctime>
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
int main () {
Something s;
for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
std::clock_t a = clock();
for (int i=0; i<1024*1024*30; ++i) ++s;
a = clock() - a;
for (int i=0; i<1024*1024*30; ++i) s++; // warm up
std::clock_t b = clock();
for (int i=0; i<1024*1024*30; ++i) s++;
b = clock() - b;
std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
<< ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
return 0;
}
// b.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
for (auto it=data.begin(), end=data.end(); it!=end; ++it)
++*it;
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
가상 머신에서 g ++ 4.5를 사용한 결과 (시간은 초 단위 임) :
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 1.70 2.39
-DPACKET_SIZE=50 -O3 0.59 1.00
-DPACKET_SIZE=500 -O1 10.51 13.28
-DPACKET_SIZE=500 -O3 4.28 6.82
이제 다음 파일을 보자.
// c.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
증분에서는 아무 것도 수행하지 않습니다. 증분의 복잡성이 일정한 경우를 시뮬레이션합니다.
결과는 이제 매우 다양합니다.
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 0.05 0.74
-DPACKET_SIZE=50 -O3 0.08 0.97
-DPACKET_SIZE=500 -O1 0.05 2.79
-DPACKET_SIZE=500 -O3 0.08 2.18
-DPACKET_SIZE=5000 -O3 0.07 21.90
이전 값이 필요하지 않은 경우 사전 증분을 사용하는 습관을들이십시오. 내장 유형과 일관성이 유지되면 익숙해지며 내장 유형을 사용자 정의 유형으로 교체하더라도 불필요한 성능 손실의 위험이 없습니다.
i++
말한다 increment i, I am interested in the previous value, though
.++i
increment i, I am interested in the current value
또는 말한다 increment i, no interest in the previous value
. 다시 말하지만 지금 당장은 아니더라도 익숙해 질 것입니다.조기 최적화는 모든 악의 근원입니다. 조기 비관 화도 마찬가지입니다.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
하고 실제 트리 구조자 (BSP, kd, Quadtree, Octree Grid 등)를 신경 쓰지 않는 깊이 우선 순회를위한 반복자를 가지고 있습니다 . 이러한 반복자는 어떤 상태, 예를 들면 유지해야 parent node
, child node
, index
같은 물건을. 대체로, 몇 가지 사례 만 존재하더라도 내 입장은 ...
컴파일러가 접미사 경우 임시 변수 사본을 최적화 할 수 없다고 말하는 것은 전적으로 올바르지 않습니다. VC를 사용한 빠른 테스트는 적어도 어떤 경우에는 그렇게 할 수 있음을 보여줍니다.
다음 예에서 생성 된 코드는 예를 들어 접두사와 접미사에 동일합니다.
#include <stdio.h>
class Foo
{
public:
Foo() { myData=0; }
Foo(const Foo &rhs) { myData=rhs.myData; }
const Foo& operator++()
{
this->myData++;
return *this;
}
const Foo operator++(int)
{
Foo tmp(*this);
this->myData++;
return tmp;
}
int GetData() { return myData; }
private:
int myData;
};
int main(int argc, char* argv[])
{
Foo testFoo;
int count;
printf("Enter loop count: ");
scanf("%d", &count);
for(int i=0; i<count; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
}
++ testFoo이든 testFoo ++이든 관계없이 여전히 동일한 결과 코드를 얻게됩니다. 실제로, 사용자로부터 카운트를 읽지 않아도 옵티마이 저는 모든 것을 일정하게 유지했습니다. 그래서 이건:
for(int i=0; i<10; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
결과는 다음과 같습니다.
00401000 push 0Ah
00401002 push offset string "Value: %d\n" (402104h)
00401007 call dword ptr [__imp__printf (4020A0h)]
따라서 접미사 버전이 느려질 수는 있지만 최적화 프로그램을 사용하지 않으면 임시 사본을 제거하기에 충분할 수 있습니다.
구글 C ++ 스타일 가이드는 말한다 :
사전 증가와 사전 감소
반복자 및 기타 템플릿 객체와 함께 증분 및 감소 연산자의 접두사 형식 (++ i)을 사용하십시오.
정의 : 변수가 증가 (++ i 또는 i ++) 또는 감소 (--i 또는 i--)되고 식의 값이 사용되지 않는 경우 사전 증가 (감소) 또는 사후 증가 (감소) 여부를 결정해야합니다.
장점 : 반환 값을 무시하면 "pre"형식 (++ i)이 "post"형식 (i ++)보다 효율적이지 않으며 종종 더 효율적입니다. 증가 후 (또는 감소) i의 사본을 작성해야하므로 표현식 값이 필요하기 때문입니다. i가 반복자이거나 다른 스칼라가 아닌 유형 인 경우 i를 복사하면 비용이 많이들 수 있습니다. 값이 무시 될 때 두 유형의 증분이 동일하게 동작하므로 항상 사전 증분하지 않는 이유는 무엇입니까?
단점 : C에서, 특히 for 루프에서 표현식 값이 사용되지 않을 때 후행 증가를 사용하는 전통이 개발되었습니다. "대상"(i)이 영어 에서처럼 "동사"(++)보다 앞에 있기 때문에 일부는 증가 후 사후를 읽기가 더 쉽다는 것을 알게됩니다.
결정 : 간단한 스칼라 (객체가 아닌) 값의 경우 하나의 형식을 선호 할 이유가 없으며 둘 중 하나를 허용합니다. 반복자 및 기타 템플릿 유형의 경우 사전 증가를 사용하십시오.
Andrew Koenig가 Code Talk에 대한 훌륭한 게시물을 최근에 지적하고 싶습니다.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
우리 회사는 또한 해당되는 경우 일관성과 성능을 위해 ++ iter 규칙을 사용합니다. 그러나 Andrew는 의도와 성능에 대해 간과 된 세부 사항을 제시합니다. 우리가 ++ iter 대신 iter ++를 사용하고 싶을 때가 있습니다.
따라서 먼저 의도를 결정하고 사전 또는 사후가 중요하지 않은 경우 추가 객체 생성을 피하고 던지므로 성능상의 이점이 있으므로 pre를 사용하십시오.
K
... 의도 대 성능에 대해 간과 된 세부 사항을 제기합니다. 우리가 ++ iter 대신 iter ++를 사용하고 싶을 때가 있습니다.
분명히 post와 pre-increment는 다른 의미를 지니고 있으며 결과가 사용될 때 적절한 연산자를 사용해야한다는 것에 모든 사람이 동의합니다. 문제는 결과가 폐기 될 때 ( for
루프 에서와 같이)해야 할 일이라고 생각합니다 . 이 질문 (IMHO)에 대한 답 은 성능 고려 사항이 무시할 수 있으므로보다 자연스러운 것을 수행해야한다는 것입니다. 나 자신 ++i
은 더 자연 스럽지만 내 경험에 따르면 소수에 있고 사용 i++
하면 코드를 읽는 대부분의 사람들에게 금속 오버 헤드가 줄어 듭니다 .
결국 그 언어는 " ++C
" 라고 불리지 않습니다 . [*]
[*] ++C
더 논리적 인 이름 에 대한 필수 토론을 삽입하십시오 .
때 사용하지 않는 반환 값을 컴파일러의 경우 임시을 사용하지 않도록 보장 ++ 전 . 더 빠르다는 보장은 없지만 느리지는 않습니다.
경우 사용 반환 값을 난 ++ 서로에 의존하지 않기 때문에, 상기 프로세서는 상기 파이프 라인으로 증가 및 좌측 모두 푸시 할 수있다. ++ i는 증분 작업이 끝날 때까지 프로세서가 왼쪽을 시작할 수 없기 때문에 파이프 라인을 중단시킬 수 있습니다. 프로세서가 다른 유용한 기능을 찾을 수 있기 때문에 파이프 라인 스톨이 보장되지 않습니다.
사이의 성능 차이 ++i
와는 i++
당신이 값을 돌려 함수로 연산자로 생각하고 그들이 어떻게 구현 될 때 더 명백 할 것이다. 무슨 일이 일어나고 있는지 이해하기 쉽게하기 위해 다음 코드 예제는 int
마치struct
.
++i
변수를 증가시킨 다음 결과 를 반환합니다. 이 작업은 적절한 시간에 최소 CPU 시간으로 수행 할 수 있으며 많은 경우 한 줄의 코드 만 필요합니다.
int& int::operator++() {
return *this += 1;
}
그러나 같은 것은 말할 수 없다 i++
.
후행 증가 i++
는 종종 증분 하기 전에 원래 값을 반환하는 것으로 간주됩니다 . 그러나 함수는 완료된 경우에만 결과를 반환 할 수 있습니다 . 결과적으로 원래 값을 포함하는 변수의 사본을 작성하고 변수를 증가시킨 다음 원래 값을 보유한 사본을 리턴해야합니다.
int int::operator++(int& _Val) {
int _Original = _Val;
_Val += 1;
return _Original;
}
사전 증분과 사후 증분간에 기능적 차이가없는 경우 컴파일러는 둘 사이의 성능 차이가 없도록 최적화를 수행 할 수 있습니다. 복합 데이터가 같은 입력 한 경우, struct
또는 class
관여, 복사 생성자는 후행 증가에 호출됩니다, 깊은 사본이 필요한 경우이 최적화를 수행 할 수 없습니다. 따라서 사전 증분은 일반적으로 사후 증분보다 빠르며 더 적은 메모리를 필요로합니다.
@ 마크 : 이전 답변이 약간 뒤집 혔기 때문에 삭제했으며 그 자체만으로도 가치가 떨어졌습니다. 실제로 많은 사람들의 마음에 무엇이 있는지 묻는다는 점에서 좋은 질문이라고 생각합니다.
일반적인 대답은 ++ i가 i ++보다 빠르다는 것입니다. 의심 할 여지없이 더 큰 질문은 "언제주의해야합니까?"입니다.
반복자를 증가시키는 데 소비 된 CPU 시간의 비율이 10 % 미만이면 신경 쓰지 않아도됩니다.
반복자를 증가시키는 데 소비 된 CPU 시간의 비율이 10 %를 초과하면 반복을 수행하는 명령문을 확인할 수 있습니다. 반복자를 사용하지 않고 정수만 증가시킬 수 있는지 확인하십시오. 가능성은 있지만, 어떤 의미에서는 바람직하지는 않지만, 이터레이터에서 소비하는 모든 시간을 절약 할 수있는 기회는 꽤 좋습니다.
반복자 증가가 시간의 90 % 이상을 소비하는 예를 보았습니다. 이 경우 정수를 늘리면 실행 시간이 기본적으로 그 양만큼 줄어 듭니다. (즉, 10 배 속도 향상)
@wilhelmtell
컴파일러는 임시를 제거 할 수 있습니다. 다른 스레드에서 그대로 :
C ++ 컴파일러는 스택 기반 임시를 제거하여 프로그램 동작이 변경 되더라도이를 제거 할 수 있습니다. VC 8 용 MSDN 링크 :
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
성능상의 이점이없는 내장 유형에서도 ++ i를 사용해야하는 이유는 좋은 습관을 만드는 것입니다.
둘 다 빠릅니다.) 프로세서에 대해 동일한 계산이 필요한 경우, 수행 순서가 다릅니다.
예를 들어, 다음 코드는
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
다음 어셈블리를 생성하십시오.
0x0000000100000f24 <main+0>: push %rbp 0x0000000100000f25 <main+1>: mov %rsp,%rbp 0x0000000100000f28 <main+4>: movl $0x0,-0x4(%rbp) 0x0000000100000f2f <main+11>: incl -0x4(%rbp) 0x0000000100000f32 <main+14>: movl $0x0,-0x8(%rbp) 0x0000000100000f39 <main+21>: incl -0x8(%rbp) 0x0000000100000f3c <main+24>: mov $0x0,%eax 0x0000000100000f41 <main+29>: leaveq 0x0000000100000f42 <main+30>: retq
a ++ 및 b ++의 경우 니모닉을 포함하므로 동일한 작업입니다.)
의도 된 질문은 결과가 사용되지 않은 시점에 관한 것입니다 (C의 질문에서 명확합니다). 질문이 "커뮤니티 위키"이므로 누군가이 문제를 해결할 수 있습니까?
조기 최적화에 대해서는 Knuth가 종종 인용됩니다. 맞습니다. 그러나 Donald Knuth는 요즘 볼 수있는 끔찍한 코드로 결코 방어하지 않을 것입니다. Java 정수 중 a = b + c를 본 적이 있습니까? 이는 3 개의 권투 / unboxing 전환에 해당합니다. 그런 것들을 피하는 것이 중요합니다. 그리고 ++ i 대신 쓸데없이 i ++를 쓰는 것은 같은 실수입니다. 편집 : phresnel이 주석에 멋지게 언급했듯이 이것은 "조기 비관 화와 마찬가지로 조기 최적화는 악의입니다"라고 요약 할 수 있습니다.
사람들이 i ++에 더 익숙하다는 사실조차도 K & R의 개념적 실수로 인한 불행한 C 유산입니다 (의도적 주장을 따르면 논리적 인 결론입니다 .K & R이 의미가 없기 때문에 K & R을 지키는 것은 무의미합니다. 훌륭하지만 언어 디자이너만큼 훌륭하지는 않습니다 .C 디자인에는 gets ()에서 strcpy ()까지, strncpy () API에 이르기까지 수많은 실수가 있습니다 (1 일 이후 strlcpy () API를 가져야했습니다) ).
Btw, 나는 ++ i를 읽는 데 성가신 것을 찾기 위해 C ++에 충분히 익숙하지 않은 사람들 중 하나입니다. 아직도, 나는 그것이 옳다는 것을 인정하기 때문에 그것을 사용합니다.
++i
더 i++
사실은 더 시원하다는 것보다) 성가신 것을 발견하지 못했지만 나머지 게시물은 내 인정을 얻습니다. 어쩌면 "조기 비관 화와 마찬가지로 조기 최적화는 나쁘다"는 말을 추가하십시오.
strncpy
당시에 사용하던 파일 시스템에서 목적을 달성했습니다. 파일 이름은 8 자 버퍼이며 null로 종료하지 않아도됩니다. 언어 진화의 미래에 40 년을 보지 못했다고 비난 할 수는 없습니다.
strlcpy()
그것이 아직 발명되지 않았다는 사실에 의해 정당화되었다.
사람들에게 지혜의 보석을 제공 할 시간;)-C ++ 접미사 증가를 접두사 증가와 거의 동일하게 만드는 간단한 트릭이 있습니다 (나 자신을 위해 발명했지만 다른 사람들의 코드에서도 보았습니다. 혼자).
기본적으로 트릭은 도우미 클래스를 사용하여 반환 후 증가를 연기하고 RAII가 구조합니다.
#include <iostream>
class Data {
private: class DataIncrementer {
private: Data& _dref;
public: DataIncrementer(Data& d) : _dref(d) {}
public: ~DataIncrementer() {
++_dref;
}
};
private: int _data;
public: Data() : _data{0} {}
public: Data(int d) : _data{d} {}
public: Data(const Data& d) : _data{ d._data } {}
public: Data& operator=(const Data& d) {
_data = d._data;
return *this;
}
public: ~Data() {}
public: Data& operator++() { // prefix
++_data;
return *this;
}
public: Data operator++(int) { // postfix
DataIncrementer t(*this);
return *this;
}
public: operator int() {
return _data;
}
};
int
main() {
Data d(1);
std::cout << d << '\n';
std::cout << ++d << '\n';
std::cout << d++ << '\n';
std::cout << d << '\n';
return 0;
}
일부 커스텀 반복자 코드를 위해 개발되었으며 런타임을 줄입니다. 접두사 대 접미사의 비용은 이제 하나의 참조이며, 이것이 많이 움직이고있는 맞춤형 연산자 인 경우 접두사와 접두사가 동일한 런타임을 얻었습니다.
++i
i++
값의 오래된 사본을 반환하지 않기 때문에 보다 빠릅니다 .
또한 더 직관적입니다.
x = i++; // x contains the old value of i
y = ++i; // y contains the new value of i
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}