사실, C ++ (11) 이후의 비용 복사 는 std::vector
대부분의 경우에 사라 졌어요.
그러나 새로운 벡터 를 생성 하는 비용 (그런 다음이를 파괴 )은 여전히 존재하며, 벡터의 용량을 재사용하려는 경우 값으로 반환하는 대신 출력 매개 변수를 사용하는 것이 여전히 유용하다는 점을 명심해야합니다 . 이것은 C ++ 핵심 지침의 F.20 에 예외로 문서화되어 있습니다.
비교해 봅시다 :
std::vector<int> BuildLargeVector1(size_t vecSize) {
return std::vector<int>(vecSize, 1);
}
와:
void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
v.assign(vecSize, 1);
}
이제이 메서드를 numIter
타이트 루프에서 호출 하고 몇 가지 작업을 수행 해야한다고 가정 합니다. 예를 들어 모든 요소의 합을 계산해 봅시다.
를 사용 BuildLargeVector1
하면 다음을 수행 할 수 있습니다.
size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
std::vector<int> v = BuildLargeVector1(vecSize);
sum1 = std::accumulate(v.begin(), v.end(), sum1);
}
를 사용 BuildLargeVector2
하면 다음을 수행 할 수 있습니다.
size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
BuildLargeVector2(/*out*/ v, vecSize);
sum2 = std::accumulate(v.begin(), v.end(), sum2);
}
첫 번째 예에서는 불필요한 동적 할당 / 할당 해제가 많이 발생하며, 두 번째 예에서는 이전 방식으로 이미 할당 된 메모리를 재사용하여 출력 매개 변수를 사용하여 방지합니다. 이 최적화가 가치가 있는지 여부는 값을 계산 / 변이하는 비용과 비교하여 할당 / 할당 해제의 상대적 비용에 따라 다릅니다.
기준
vecSize
및 의 값을 가지고 놀아 봅시다 numIter
. vecSize * numIter를 일정하게 유지하여 "이론상"동일한 시간이 걸리고 (= 정확히 동일한 값을 가진 동일한 수의 할당 및 추가가 있음) 시간 차이는 다음 비용에서만 발생할 수 있습니다. 할당, 할당 해제 및 캐시의 더 나은 사용.
더 구체적으로, vecSize * numIter = 2 ^ 31 = 2147483648을 사용하겠습니다. 왜냐하면 16GB의 RAM이 있고이 숫자는 8GB 이하가 할당되도록 보장하기 때문입니다 (sizeof (int) = 4). 다른 모든 프로그램은 닫 혔고 테스트를 실행할 때 15GB를 사용할 수있었습니다.)
다음은 코드입니다.
#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>
class Timer {
using clock = std::chrono::steady_clock;
using seconds = std::chrono::duration<double>;
clock::time_point t_;
public:
void tic() { t_ = clock::now(); }
double toc() const { return seconds(clock::now() - t_).count(); }
};
std::vector<int> BuildLargeVector1(size_t vecSize) {
return std::vector<int>(vecSize, 1);
}
void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
v.assign(vecSize, 1);
}
int main() {
Timer t;
size_t vecSize = size_t(1) << 31;
size_t numIter = 1;
std::cout << std::setw(10) << "vecSize" << ", "
<< std::setw(10) << "numIter" << ", "
<< std::setw(10) << "time1" << ", "
<< std::setw(10) << "time2" << ", "
<< std::setw(10) << "sum1" << ", "
<< std::setw(10) << "sum2" << "\n";
while (vecSize > 0) {
t.tic();
size_t sum1 = 0;
{
for (int i = 0; i < numIter; ++i) {
std::vector<int> v = BuildLargeVector1(vecSize);
sum1 = std::accumulate(v.begin(), v.end(), sum1);
}
}
double time1 = t.toc();
t.tic();
size_t sum2 = 0;
{
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
BuildLargeVector2(/*out*/ v, vecSize);
sum2 = std::accumulate(v.begin(), v.end(), sum2);
}
} // deallocate v
double time2 = t.toc();
std::cout << std::setw(10) << vecSize << ", "
<< std::setw(10) << numIter << ", "
<< std::setw(10) << std::fixed << time1 << ", "
<< std::setw(10) << std::fixed << time2 << ", "
<< std::setw(10) << sum1 << ", "
<< std::setw(10) << sum2 << "\n";
vecSize /= 2;
numIter *= 2;
}
return 0;
}
결과는 다음과 같습니다.
$ g++ -std=c++11 -O3 main.cpp && ./a.out
vecSize, numIter, time1, time2, sum1, sum2
2147483648, 1, 2.360384, 2.356355, 2147483648, 2147483648
1073741824, 2, 2.365807, 1.732609, 2147483648, 2147483648
536870912, 4, 2.373231, 1.420104, 2147483648, 2147483648
268435456, 8, 2.383480, 1.261789, 2147483648, 2147483648
134217728, 16, 2.395904, 1.179340, 2147483648, 2147483648
67108864, 32, 2.408513, 1.131662, 2147483648, 2147483648
33554432, 64, 2.416114, 1.097719, 2147483648, 2147483648
16777216, 128, 2.431061, 1.060238, 2147483648, 2147483648
8388608, 256, 2.448200, 0.998743, 2147483648, 2147483648
4194304, 512, 0.884540, 0.875196, 2147483648, 2147483648
2097152, 1024, 0.712911, 0.716124, 2147483648, 2147483648
1048576, 2048, 0.552157, 0.603028, 2147483648, 2147483648
524288, 4096, 0.549749, 0.602881, 2147483648, 2147483648
262144, 8192, 0.547767, 0.604248, 2147483648, 2147483648
131072, 16384, 0.537548, 0.603802, 2147483648, 2147483648
65536, 32768, 0.524037, 0.600768, 2147483648, 2147483648
32768, 65536, 0.526727, 0.598521, 2147483648, 2147483648
16384, 131072, 0.515227, 0.599254, 2147483648, 2147483648
8192, 262144, 0.540541, 0.600642, 2147483648, 2147483648
4096, 524288, 0.495638, 0.603396, 2147483648, 2147483648
2048, 1048576, 0.512905, 0.609594, 2147483648, 2147483648
1024, 2097152, 0.548257, 0.622393, 2147483648, 2147483648
512, 4194304, 0.616906, 0.647442, 2147483648, 2147483648
256, 8388608, 0.571628, 0.629563, 2147483648, 2147483648
128, 16777216, 0.846666, 0.657051, 2147483648, 2147483648
64, 33554432, 0.853286, 0.724897, 2147483648, 2147483648
32, 67108864, 1.232520, 0.851337, 2147483648, 2147483648
16, 134217728, 1.982755, 1.079628, 2147483648, 2147483648
8, 268435456, 3.483588, 1.673199, 2147483648, 2147483648
4, 536870912, 5.724022, 2.150334, 2147483648, 2147483648
2, 1073741824, 10.285453, 3.583777, 2147483648, 2147483648
1, 2147483648, 20.552860, 6.214054, 2147483648, 2147483648
(Intel i7-7700K @ 4.20GHz, 16GB DDR4 2400Mhz, Kubuntu 18.04)
표기법 : mem (v) = v.size () * sizeof (int) = v.size () * 4 on my platform.
당연히 numIter = 1
(즉, mem (v) = 8GB) 시간이 완벽하게 동일합니다. 실제로 두 경우 모두 메모리에 8GB의 거대한 벡터를 한 번만 할당합니다. 이것은 또한 BuildLargeVector1 ()을 사용할 때 복사가 발생하지 않았 음을 증명합니다. 복사를 수행하기에 충분한 RAM이 없을 것입니다!
인 numIter = 2
경우 두 번째 벡터를 다시 할당하는 대신 벡터 용량을 재사용하면 1.37 배 더 빠릅니다.
경우 numIter = 256
(... 반복해서 256 회 벡터를 할당 해제 / 대신 할당) 벡터 용량을 재사용하는 것은 빠른 2.45x이다 :
우리는 시간 1가에서 거의 일정한 것을 알 수 있습니다 numIter = 1
에 numIter = 256
8기가바이트 중 하나 큰 벡터를 할당하는 것은 거의 32메가바이트의 256 벡터를 할당하는만큼 비용이 많이 드는 것을 의미한다. 그러나 8GB의 거대한 벡터 하나를 할당하는 것은 32MB의 벡터 하나를 할당하는 것보다 확실히 더 비싸므로 벡터의 용량을 재사용하면 성능이 향상됩니다.
에서 numIter = 512
(MEM (V) = 16메가바이트)에 numIter = 8M
(MEM은 (V) = 1KB) 달콤한 장소입니다 : 두 가지 방법이 빠르게 numIter 및 vecSize의 모든 다른 조합보다 더 빨리 정확하게, 그리고. 이것은 아마도 내 프로세서의 L3 캐시 크기가 8MB라는 사실과 관련이 있으므로 벡터가 캐시에 거의 완벽하게 맞습니다. 나는 time1
mem (v) = 16MB 의 갑작스러운 점프가 왜 mem (v) = 8MB 인 직후에 일어나는 것이 더 논리적으로 보일지 설명하지 않습니다 . 놀랍게도이 스위트 스팟에서는 용량을 재사용하지 않는 것이 실제로 약간 더 빠릅니다! 나는 이것을 정말로 설명하지 않는다.
numIter > 8M
상황이 추악 해지기 시작할 때 . 두 방법 모두 느려지지만 값으로 벡터를 반환하는 것은 더 느려집니다. 최악의 경우 단일 단일을 포함하는 벡터의 경우 int
값으로 반환하는 대신 용량을 재사용하는 것이 3.3 배 더 빠릅니다. 아마도 이것은 지배하기 시작하는 malloc ()의 고정 비용 때문일 것입니다.
time2에 대한 곡선이 time1에 대한 곡선보다 더 매끄럽다는 점에 유의하십시오. 벡터 용량을 재사용하는 것이 일반적으로 더 빠를뿐만 아니라 더 중요한 것은 더 예측 가능 하다는 것입니다 .
또한 스윗 스팟에서는 ~ 0.5 초 내에 20 억 개의 64 비트 정수를 추가 할 수 있었는데, 이는 4.2Ghz 64 비트 프로세서에서 매우 최적입니다. 8 개의 코어를 모두 사용하기 위해 계산을 병렬화하면 더 잘할 수 있습니다 (위의 테스트에서는 한 번에 하나의 코어 만 사용하며 CPU 사용량을 모니터링하면서 테스트를 다시 실행하여 확인했습니다). 최고의 성능은 mem (v) = 16kB 일 때 달성되며, 이는 L1 캐시의 크기입니다 (i7-7700K의 L1 데이터 캐시는 4x32kB).
물론 데이터에 대해 실제로 수행해야하는 계산이 많을수록 차이는 점점 더 관련성이 낮아집니다. 우리가 대체 할 경우 아래의 결과입니다 sum = std::accumulate(v.begin(), v.end(), sum);
으로 for (int k : v) sum += std::sqrt(2.0*k);
:
결론
- 값으로 반환하는 대신 출력 매개 변수를 사용하면 용량을 재사용하여 성능을 향상 시킬 수 있습니다 .
- 최신 데스크톱 컴퓨터에서는 큰 벡터 (> 16MB)와 작은 벡터 (<1kB)에만 적용 할 수 있습니다.
- 수백만 / 십억 개의 작은 벡터 (<1kB)를 할당하지 마십시오. 가능하면 용량을 재사용하거나 더 나은 방법으로 아키텍처를 다르게 설계하십시오.
다른 플랫폼에서는 결과가 다를 수 있습니다. 평소처럼 성능이 중요한 경우 특정 사용 사례에 대한 벤치 마크를 작성하십시오.