C ++ 벡터에서 push_back이 상각되는 이유는 무엇입니까?


23

저는 C ++을 배우고 있으며 벡터에 대한 push_back 함수의 실행 시간이 "amortized"라는 것을 알았습니다. 또한이 문서에는 "재 할당이 발생하면 재 할당 자체가 전체 크기에 따라 선형으로 이루어집니다."

이것이 push_back 함수가 이라는 것을 의미하지 않아야합니까 , 여기서 은 벡터의 길이입니까? 결국 최악의 분석에 관심이 있습니다.O(n)n

나는 형용사 "암부 한"이 달리는 시간을 어떻게 변화시키는 지 이해하지 못한다고 생각합니다.


RAM 시스템에서 바이트의 메모리를 할당 하는 것은 연산 이 아니며 거의 일정한 시간으로 간주됩니다. nO(n)
usul

답변:


24

여기서 중요한 단어는 "암살"입니다. 할부 상환 분석은 일련의 연산 을 검사하는 분석 기술입니다 . 전체 시퀀스가 시간에 실행되면 시퀀스의 각 작업이 실행됩니다 . 아이디어는 시퀀스의 몇 가지 작업이 비용이 많이 들지만 프로그램을 계량하기에 충분하지 않을 수 있다는 것입니다. 이는 일부 입력 분포 또는 무작위 분석에 대한 평균 사례 분석과 다르다는 점에 유의해야합니다. 할부 상환 분석 은 입력에 관계없이 알고리즘의 성능에 대한 최악의 경우를 설정했습니다 . 프로그램 전반에 걸쳐 지속적인 상태를 유지하는 데이터 구조를 분석하는 데 가장 일반적으로 사용됩니다.nT(n)T(n)/n

주어진 가장 일반적인 예 중 하나는 요소 를 표시하는 멀티 팝 연산으로 스택을 분석하는 것입니다 . 멀티 팝에 대한 순진한 분석에 따르면 최악의 경우 멀티 팝은 스택의 모든 요소를 튀어 나와야하기 때문에 시간이 걸린다고합니다 . 그러나 일련의 작업을 보면 팝 수는 푸시 수를 초과 할 수 없습니다. 따라서 임의의 연산 시퀀스에서 팝 수는 초과 할 수 없으므로 멀티 팝은 상각 된 시간에 실행됩니다 .kO(n)nO(n)O(1)

이제 이것이 C ++ 벡터와 어떤 관련이 있습니까? 벡터는 배열로 구현되므로 벡터의 크기를 늘리려면 메모리를 다시 할당하고 전체 배열을 복사해야합니다. 분명히 우리는 이것을 매우 자주하고 싶지 않을 것입니다. 따라서 push_back 연산을 수행하고 벡터가 더 많은 공간을 할당해야하는 경우 크기는 인수 만큼 증가합니다 . 이제는 더 많은 메모리가 필요하므로 전체를 사용하지는 않지만 다음 몇 번의 push_back 작업은 모두 일정한 시간에 실행됩니다.m

이제 push_back 연산의 할부 상환 분석 ( 여기서 찾은 )을 수행하면 상각 시간이 일정하게 실행되는 것을 알 수 있습니다. 항목이 있고 곱셈 계수가 이라고 가정합니다 . 재배치 횟수는 대략 입니다. 번째 재 할당에 비례하여 비용 현재 배열의 크기. 따라서 푸시 백의 총 시간 은 기하학적 계열 이므로 입니다. 이것을 연산으로 나누면 각 연산에 이 걸립니다.nmlogm(n)imini=1logm(n)minmm1nmm1상수입니다. 마지막으로 요인 선택에주의해야합니다 . 너무 가까운 경우이 상수는 실제 응용 프로그램에 비해 너무 커지지 만 이 너무 크면 (2), 많은 메모리를 낭비하기 시작합니다. 이상적인 성장률은 응용 프로그램에 따라 다르지만 일부 구현에서는 사용한다고 생각 합니다.m1m1.5


12

@Marc은 (내 생각에 따라) 훌륭한 분석을 제공했지만 일부 사람들은 약간 다른 각도에서 사물을 고려하는 것을 선호 할 수 있습니다.

하나는 재 할당을 수행하는 약간 다른 방법을 고려하는 것입니다. 이전 스토리지에서 새 스토리지로 모든 요소를 ​​즉시 복사하는 대신 한 번에 하나의 요소 만 복사하는 것이 좋습니다. 즉, push_back을 수행 할 때마다 새 요소가 새 공간에 추가되고 기존 요소는 정확히 하나만 복사됩니다. 이전 공간에서 새 공간으로의 요소. 성장률이 2라고 가정하면 새 공간이 가득 차면 모든 요소를 ​​이전 공간에서 새 공간으로 복사했음을 알았으며 각 push_back은 정확히 일정한 시간이었습니다. 이 시점에서 이전 공간을 버리고 두 배나 큰 게인 인 새로운 메모리 블록을 할당하고 프로세스를 반복합니다.

분명히, 우리는 이것을 무한정 계속 사용할 수 있습니다 (또는 사용 가능한 메모리가있는 한). 모든 push_back에는 새로운 요소 하나를 추가하고 하나의 오래된 요소를 복사하는 것이 포함됩니다.

일반적인 구현에는 여전히 동일한 수의 사본이 있지만 한 번에 하나씩 사본을 수행하는 대신 모든 기존 요소를 한 번에 복사합니다. 한편으로는, 맞습니다. 즉, push_back의 개별 호출을 보면 일부가 다른 것보다 상당히 느리다는 것을 의미합니다. 그러나 장기 평균을 보면 벡터의 크기에 관계없이 push_back 호출 당 수행되는 복사량은 일정하게 유지됩니다.

계산 복잡성과 관련이 없지만 push_back 당 하나의 요소를 복사하는 대신 push_back 당 시간이 일정하게 유지되는 대신 작업을 수행하는 것이 유리한 이유를 지적하는 것이 좋습니다. 고려해야 할 이유가 3 가지 이상 있습니다.

첫 번째는 단순히 메모리 가용성입니다. 이전 메모리는 복사가 완료된 후에 만 ​​다른 용도로 사용할 수 있습니다. 한 번에 하나의 항목 만 복사하면 이전 메모리 블록이 훨씬 더 오래 할당됩니다. 실제로, 항상 하나의 오래된 블록과 하나의 새로운 블록이 항상 할당됩니다. 2보다 작은 성장 인자 (일반적으로 원하는)를 결정했다면 항상 더 많은 메모리를 할당해야합니다.

둘째, 한 번에 하나의 이전 요소 만 복사 한 경우 배열에 대한 색인 작성은 약간 까다로울 수 있습니다. 각 색인 작업은 주어진 색인의 요소가 현재 이전 메모리 블록에 있는지 또는 새로운. 어떤 방법으로도 복잡하지는 않지만 배열 인덱싱과 같은 기본 작업의 경우 거의 모든 속도 저하가 심각 할 수 있습니다.

셋째, 한 번에 모두 복사하면 캐싱을 훨씬 더 잘 활용할 수 있습니다. 한 번에 모두 복사하면 대부분의 경우 소스와 대상이 모두 캐시에있을 것으로 예상 할 수 있으므로 캐시 누락 비용은 캐시 라인에 맞는 요소 수만큼 상각됩니다. 한 번에 하나의 요소를 복사하면 복사하는 모든 요소에 대해 캐시 누락이 쉽게 발생할 수 있습니다. 이는 복잡성이 아니라 상수 요소 만 변경하지만 여전히 상당히 중요 할 수 있습니다. 일반적인 머신의 경우 10 ~ 20의 요소를 쉽게 기대할 수 있습니다.

실시간 요구 사항이있는 시스템을 설계하는 경우 한 번에 하나의 요소 만 복사하는 것이 합리적 일 수 있습니다. 전체 속도는 낮을 수도 있고 아닐 수도 있지만, 한 번의 push_back 실행에 걸리는 시간에는 여전히 상한이 있습니다. 실시간 할당자가 있다고 가정합니다 (물론 많은 실시간 시스템은 단순히 실시간 요구 사항이있는 부분에서 메모리의 동적 할당을 금지합니다.


2
+1 멋진 Feynman 스타일의 설명입니다.
Monica Reinstate Monica
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.