더 빠름 : 스택 할당 또는 힙 할당


503

이 질문은 초등하게 들릴지 모르지만 이것은 내가 일하는 다른 개발자와의 토론입니다.

나는 힙을 할당하는 대신 가능한 한 할당 할당을 처리하려고했습니다. 그는 나에게 말을 걸고 어깨 너머로보고 있었고, 그들이 같은 성능으로 현명하기 때문에 필요하지 않다고 말했다.

필자는 항상 스택을 늘리는 것이 일정한 시간이라는 인상을 받았으며 힙 할당의 성능은 할당 (적절한 크기의 구멍 찾기)과 할당 해제 (단편화를위한 구멍 축소)에 대한 힙의 현재 복잡성에 달려 있습니다. 많은 표준 라이브러리 구현은 실수하지 않으면 삭제 중에이 작업을 수행하는 데 시간이 걸립니다.

이것은 아마도 컴파일러에 매우 의존적 일 것입니다. 이 프로젝트에서는 특히 PPC 아키텍처에 Metrowerks 컴파일러를 사용하고 있습니다. 이 조합에 대한 통찰력이 가장 도움이되지만 일반적으로 GCC 및 MSVC ++에 대해서는 어떤 경우가 있습니까? 힙 할당이 스택 할당만큼 성능이 좋지 않습니까? 차이가 없습니까? 또는 차이가 너무 작아서 무의미한 미세 최적화가됩니다.


11
나는 이것이 꽤 오래되었다는 것을 알고 있지만, 다른 종류의 할당을 보여주는 C / C ++ 스 니펫을 보는 것이 좋을 것입니다.
Joseph Weissman 2016 년

42
당신의 젖소 주인은 끔찍한 무지하지만 더 중요한 것은 그가 무지한 것에 대해 권위있는 주장을하기 때문에 위험합니다. 그러한 사람들을 가능한 빨리 팀에서 제외 시키십시오.
짐 발터

5
힙은 일반적으로 스택보다 훨씬 큽니다. 많은 양의 데이터가 할당 된 경우 실제로 데이터를 힙에 배치하거나 OS에서 스택 크기를 변경해야합니다.
Paul Draper

1
기본적으로 무의미한 미세 최적화를 통해 벤치 마크 또는 복잡성을 입증하지 않는 한 모든 최적화가 이루어집니다.
Björn Lindqvist

2
동료에게 Java 또는 C # 경험이 있는지 궁금합니다. 이러한 언어에서는 거의 모든 것이 힙 아래에 할당되어 이러한 가정으로 이어질 수 있습니다.
Cort Ammon

답변:


493

실제로 스택 할당은 스택 포인터를 이동하기 때문에 스택 할당이 훨씬 빠릅니다. 메모리 풀을 사용하면 힙 할당에서 비슷한 성능을 얻을 수 있지만 약간의 복잡성과 자체 두통이 발생합니다.

또한 스택 대 힙은 성능 고려 사항 일뿐만 아니라; 또한 예상되는 개체 수명에 대해 많은 정보를 제공합니다.


211
더 중요한 것은 스택이 항상 뜨겁다는 것입니다. 얻는 메모리는 먼 힙 할당 메모리보다 캐시에있을 가능성이 높습니다
Benoît

47
일부 (주로 알고있는) 아키텍처에서 스택은 빠른 온 다이 메모리 (예 : SRAM)에 저장 될 수 있습니다. 이것은 큰 차이를 만들 수 있습니다!
leander

38
스택은 실제로 스택이기 때문에. 스택이 사용하지 않는 한 스택에서 사용하는 메모리 청크를 해제 할 수 없습니다. 관리가 없습니다, 당신은 그것에 물건을 밀거나 터뜨립니다. 반면에 힙 메모리는 관리됩니다. 커널에 메모리 청크를 요청하고, 분할하고, 병합하고, 재사용하고, 해제합니다. 스택은 실제로 빠르고 짧은 할당을위한 것입니다.
Benoît

24
@Pacerier 스택이 힙보다 훨씬 작기 때문입니다. 큰 배열을 할당하려면 힙에 배열을 할당하는 것이 좋습니다. 스택에 큰 배열을 할당하려고하면 스택 오버플로가 발생합니다. 예를 들어 C ++에서 다음을 시도하십시오. int t [100000000]; 예를 들어보십시오. t [10000000] = 10; 그리고 cout << t [10000000]; 스택 오버플로가 발생하거나 작동하지 않으며 아무것도 표시하지 않습니다. 그러나 힙에 배열을 할당하면 : int * t = new int [100000000]; 힙이 큰 배열에 필요한 크기를 갖기 때문에 동일한 작업을 수행 한 후에 작동합니다.
릴리안 A. 모라 루

7
@Pacerier 가장 확실한 이유는 스택의 객체가 할당 된 블록을 종료 할 때 범위를 벗어난 것입니다.
Jim Balter

166

스택이 훨씬 빠릅니다. 문자 그대로 대부분의 아키텍처, 예를 들어 x86에서 단일 명령어 만 사용합니다.

sub esp, 0x10

(이것은 스택 포인터를 0x10 바이트 아래로 이동시켜 변수에 의해 사용되도록 해당 바이트를 "할당"합니다.)

물론 스택 할당을 과도하게 사용하거나 재귀를 시도하면 신속하게 알 수 있으므로 스택 크기는 매우 유한합니다.

또한 프로파일 링을 통해 입증 할 수없는 코드 성능을 최적화 할 이유가 거의 없습니다. "조기 최적화"는 종종 가치보다 더 많은 문제를 일으 킵니다.

내 경험 법칙 : 컴파일 타임에 일부 데이터가 필요하다는 것을 알고 있으며 크기가 수백 바이트 미만이면 스택 할당합니다. 그렇지 않으면 힙 할당합니다.


20
하나의 명령이며 일반적으로 스택의 모든 객체가 공유합니다.
MSalters

9
요점을 잘, 특히 검증 가능하게 요점을 지적했습니다. 나는 성과에 대한 사람들의 염려가 어떻게 잘못되었는지에 대해 계속 놀랐다.
Mike Dunlavey가

6
"할당 해제"도 매우 간단하며 단일 leave명령으로 수행됩니다 .
doc

15
여기에서 "숨겨진"비용, 특히 처음으로 스택을 확장 할 때의 비용을 명심하십시오. 그렇게하면 페이지 오류, 커널로의 컨텍스트 전환이 발생하여 메모리를 할당하거나 최악의 경우 스왑에서로드하는 작업이 필요합니다.
nos

2
경우에 따라 0 명령어로 할당 할 수도 있습니다. 할당해야 할 바이트 수에 대한 정보가 있으면 컴파일러는 다른 스택 변수를 할당하는 동시에 미리 할당 할 수 있습니다. 이 경우, 당신은 전혀 지불하지 않습니다!
Cort Ammon

119

솔직히 성능을 비교하는 프로그램을 작성하는 것은 쉽지 않습니다.

#include <ctime>
#include <iostream>

namespace {
    class empty { }; // even empty classes take up 1 byte of space, minimum
}

int main()
{
    std::clock_t start = std::clock();
    for (int i = 0; i < 100000; ++i)
        empty e;
    std::clock_t duration = std::clock() - start;
    std::cout << "stack allocation took " << duration << " clock ticks\n";
    start = std::clock();
    for (int i = 0; i < 100000; ++i) {
        empty* e = new empty;
        delete e;
    };
    duration = std::clock() - start;
    std::cout << "heap allocation took " << duration << " clock ticks\n";
}

어리석은 일관성은 작은 마음의 홉 고블린 이라고합니다 . 분명히 컴파일러를 최적화하는 것은 많은 프로그래머의 마음에 얽힌 것입니다. 이 토론은 답변의 맨 아래에 있었지만 사람들은 분명히 그 내용을 읽지 않아도되므로 이미 대답 한 질문을 피하기 위해 여기로 이동하고 있습니다.

최적화 컴파일러는이 코드가 아무 작업도하지 않으며 모든 것을 최적화 할 수 있습니다. 그런 일을하는 것은 옵티마이 저의 일이며, 옵티 마이저와 싸우는 것은 어리석은 일입니다.

현재 사용중인 모든 옵티 마이저를 속일 수있는 미래의 방법이 없기 때문에 최적화가 해제 된 상태 에서이 코드를 컴파일하는 것이 좋습니다.

옵티 마이저를 켜고 싸우는 것에 대해 불평하는 사람은 누구나 공개 조롱을 받아야합니다.

나노초 정밀도에 관심이 있다면 사용하지 않을 것입니다 std::clock() 입니다. 박사 학위 논문으로 결과를 게시하고 싶다면 이것에 대해 더 많이 다루고 GCC, Tendra / Ten15, LLVM, Watcom, Borland, Visual C ++, Digital Mars, ICC 및 기타 컴파일러를 비교할 것입니다. 실제로 힙 할당은 스택 할당보다 수백 배 더 오래 걸리며 더 이상 질문을 조사하는 데 유용한 정보가 없습니다.

최적화 프로그램은 테스트중인 코드를 제거하는 사명을 가지고 있습니다. 옵티 마이저에게 실행을 지시 한 다음 옵티 마이저를 실제로 최적화하지 않도록 속이려 할 이유가 없습니다. 그러나 그렇게하는 데 가치가 있으면 다음 중 하나 이상을 수행합니다.

  1. 에 데이터 멤버를 추가 empty하고 루프에서 해당 데이터 멤버에 액세스하십시오. 그러나 데이터 멤버에서만 읽은 경우 옵티마이 저는 일정한 폴딩을 수행하고 루프를 제거 할 수 있습니다. 데이터 멤버에만 쓰면 옵티마이 저는 루프의 마지막 반복을 제외한 모든 것을 건너 뛸 수 있습니다. 또한이 질문은 "스택 할당 및 데이터 액세스 대 힙 할당 및 데이터 액세스"가 아닙니다.

  2. 선언 e volatile, 하지만 volatile종종 잘못 컴파일 (PDF)를.

  3. e루프 내부 의 주소를 가져와 extern다른 파일에 선언 되고 정의 된 변수에 할당하십시오 . 그러나이 경우에도 컴파일러는 적어도 스택에서 e항상 동일한 메모리 주소에 할당 된 다음 위의 (1)과 같이 일정한 접기를 수행 한다는 것을 알 수 있습니다. 루프의 모든 반복을 얻지 만 객체는 실제로 할당되지 않습니다.

명백히,이 테스트는 할당과 할당 해제를 측정한다는 점에서 결함이 있으며, 원래 질문은 할당 해제에 대해 묻지 않았습니다. 물론 스택에 할당 된 변수는 해당 범위의 끝에서 자동으로 할당이 해제되므로 delete(1) 숫자를 기울이지 않습니다 (스택 할당 해제는 스택 할당에 대한 숫자에 포함되므로 힙 할당 해제를 측정하는 것은 공정합니다). 2) delete시간 측정을 한 후에 새로운 포인터에 대한 참조를 유지하고 호출하지 않는 한 꽤 나쁜 메모리 누수가 발생 합니다.

내 컴퓨터에서 Windows에서 g ++ 3.4.4를 사용하면 100000 미만의 할당에 대해 스택 및 힙 할당 모두에 대해 "0 클럭 틱"이 표시되고 스택 할당 및 "15 클럭 틱에 대해"0 클럭 틱 "이 표시됩니다. 힙 할당의 경우 " 10,000,000 개의 할당을 측정 할 때 스택 할당에는 31 개의 클럭 틱이, 힙 할당에는 1562 개의 클럭 틱이 필요합니다.


예, 최적화 컴파일러는 빈 객체를 생성하지 않아도됩니다. 올바르게 이해하면 전체 첫 번째 루프를 제거 할 수도 있습니다. 10,000,000 스택 할당으로 반복을 부딪쳤을 때 31 클럭 틱이 걸렸고 힙 할당에는 1562 클럭 틱이 걸렸습니다. g ++에게 실행 파일을 최적화하도록 지시하지 않으면 g ++이 생성자를 생략하지 않았다고 말하는 것이 안전하다고 생각합니다.


필자가 작성한 이후 몇 년 동안 Stack Overflow는 최적화 된 빌드에서 성능을 게시하는 것이 좋습니다. 일반적으로 이것이 맞다고 생각합니다. 그러나 실제로 코드 최적화를 원하지 않는 경우 컴파일러에게 코드 최적화를 요청하는 것은 어리석은 일이라고 생각합니다. 그것은 주차 대행에 대한 추가 비용을 지불하는 것과 매우 유사하지만 열쇠를 넘기는 것을 거부합니다. 이 특별한 경우에는 최적화 프로그램을 실행하고 싶지 않습니다.

약간 수정 된 벤치 마크 버전을 사용하여 (원본 프로그램이 루프를 통해 매번 스택에 무언가를 할당하지 않은 유효 지점을 해결하기 위해) 최적화없이 컴파일하지만 라이브러리를 릴리스하기 위해 링크 (유효한 지점을 해결하기 위해) 디버그 라이브러리에 연결하여 발생하는 속도 저하를 포함시키지 않으려는 경우)

#include <cstdio>
#include <chrono>

namespace {
    void on_stack()
    {
        int i;
    }

    void on_heap()
    {
        int* i = new int;
        delete i;
    }
}

int main()
{
    auto begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_stack();
    auto end = std::chrono::system_clock::now();

    std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());

    begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_heap();
    end = std::chrono::system_clock::now();

    std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
    return 0;
}

표시합니다 :

on_stack took 2.070003 seconds
on_heap took 57.980081 seconds

커맨드 라인으로 컴파일 할 때 내 시스템에서 cl foo.cc /Od /MT /EHsc.

최적화되지 않은 빌드를 얻는 방법에 동의하지 않을 수 있습니다. 괜찮습니다. 원하는만큼 벤치 마크를 자유롭게 수정하십시오. 최적화를 켜면 다음과 같은 결과가 나타납니다.

on_stack took 0.000000 seconds
on_heap took 51.608723 seconds

스택 할당이 실제로 즉각적이기 때문이 아니라 절반 정도의 컴파일러가 on_stack유용한 것을 수행하지 않고 최적화 할 수 있다는 것을 알 수 있기 때문 입니다. 내 Linux 랩톱의 GCC는 on_heap유용한 기능을 수행하지 않으며이를 최적화합니다.

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds

2
또한 주 함수의 맨 처음에 "교정"루프를 추가해야합니다. 루프주기 당 시간이 얼마나 걸리는지 파악하고 다른 루프를 조정하여 예제가 제대로 실행되도록해야합니다. 사용중인 고정 상수 대신 일정 시간.
Joe Pineda

2
또한 각 옵션 루프가 실행되는 횟수를 늘리고 (g ++에 최적화하지 말라고 지시합니까?) 상당한 결과를 얻었습니다. 이제 스택이 더 빠르다는 어려운 사실이 있습니다. 당신의 노력에 감사드립니다!
Joe Pineda

7
이와 같은 코드를 제거하는 것은 옵티마이 저의 작업입니다. 옵티 마이저를 켜고 실제로 최적화하지 못하게하는 좋은 이유가 있습니까? 옵티 마이저와 싸우는 것을 즐긴다면 똑똑한 컴파일러 작성자가 어떻게되는지 배울 준비를하십시오.
Max Lybbert

3
나는 매우 늦었지만 힙 할당이 커널을 통해 메모리를 요청한다는 사실을 언급 할 가치가 있으므로 성능 적중은 커널의 효율성에 달려 있습니다. 이 코드를 Linux (Linux 3.10.7-gentoo # 2 SMP Wed Sep 4 18:58:21 MDT 2013 x86_64)와 함께 사용하고 HR 타이머를 수정하고 각 루프에서 1 억 반복을 사용하면 다음 stack allocation took 0.15354 seconds, heap allocation took 0.834044 seconds과 같은 성능을 얻을 -O0수 있습니다. Linux 힙 할당은 특정 머신에서 약 5.5 배로 느려집니다.
Taywee

4
최적화가없는 창 (디버그 빌드)에서는 디버그 힙을 사용합니다. 디버그 힙은 비 디버그 힙보다 훨씬 느립니다. 옵티 마이저를 전혀 "트릭"하는 것이 나쁜 생각은 아닙니다. 컴파일러 작성자는 똑똑하지만 컴파일러는 AI가 아닙니다.
paulm

30

다른 멀티 코어 시스템에도 적용 할 수있는 Xbox 360 크세논 프로세서의 스택 대 힙 할당에 대해 흥미로운 점은 힙에 할당하면 중요 섹션이 다른 모든 코어를 중지하도록 할당되어 할당자가 수행 할 수 있다는 것입니다. 충돌하지 않습니다. 따라서 타이트한 루프에서 스택 할당은 실속을 막기 때문에 고정 크기의 배열로가는 길이었습니다.

멀티 코어 / 멀티 프로세스를 코딩하는 경우 고려할 또 다른 속도 향상 일 수 있습니다. 스택 할당은 범위 지정된 기능을 실행하는 코어에서만 볼 수 있으며 다른 코어 / CPU에는 영향을 미치지 않습니다.


4
그것은 크세논뿐만 아니라 대부분의 멀티 코어 머신에서도 마찬가지입니다. 해당 PPU 코어에서 두 개의 하드웨어 스레드를 실행 중일 수 있으므로 Cell조차도 그렇게해야합니다.
Crashworks

15
이는 힙 할당 자의 (특히 열악한) 구현의 영향입니다. 더 나은 힙 할당자는 모든 할당에서 잠금을 획득 할 필요가 없습니다.
Chris Dodd

19

성능이 뛰어난 특정 크기의 객체에 대해 특수 힙 할당자를 작성할 수 있습니다. 그러나 일반적인 힙 할당자는 특히 성능이 좋지 않습니다.

또한 개체의 예상 수명에 대해 Torbjörn Gyllebring에 동의합니다. 좋은 지적!


1
때때로 슬랩 할당이라고도합니다.
Benoit

8

스택 할당과 힙 할당이 일반적으로 호환되지 않는다고 생각합니다. 또한 두 가지 성능 모두 일반적인 용도로 충분하기를 바랍니다.

나는 작은 아이템들 중 어느 것이 든 할당 범위에 더 적합한 것을 강력히 추천합니다. 큰 품목의 경우 힙이 필요할 수 있습니다.

여러 스레드가있는 32 비트 운영 체제에서는 주소 공간을 조각해야하며 조만간 하나의 스레드 스택이 다른 스레드 스택으로 실행되기 때문에 스택이 다소 제한적입니다 (일반적으로 최소한 몇 mb 이상임). 단일 스레드 시스템 (Linux glibc 단일 스레드)에서는 스택이 커지고 커질 수 있기 때문에 제한이 훨씬 적습니다.

64 비트 운영 체제에는 스레드 스택을 상당히 크게 만들 수있는 충분한 주소 공간이 있습니다.


6

일반적으로 스택 할당은 스택 포인터 레지스터에서 빼기로 구성됩니다. 힙을 검색하는 것보다 훨씬 빠릅니다.

때때로 스택 할당에는 가상 메모리 페이지를 추가해야합니다. 0으로 된 메모리의 새 페이지를 추가 할 때는 디스크에서 페이지를 읽을 필요가 없으므로 일반적으로 힙을 검색하는 것 (특히 힙의 일부가 페이지 아웃 된 경우)보다 훨씬 빠릅니다. 드문 경우이지만 이러한 예제를 구성 할 수 있습니다. 이미 RAM에있는 힙 부분에서 충분한 공간을 사용할 수 있지만 스택에 새 페이지를 할당하면 다른 페이지가 작성 될 때까지 기다려야합니다. 디스크에. 드문 상황에서 힙이 더 빠릅니다.


힙이 페이징되지 않으면 힙이 "검색"된 것으로 생각하지 않습니다. 솔리드 스테이트 메모리는 멀티플렉서를 사용하고 메모리에 직접 액세스 할 수 있으므로 랜덤 액세스 메모리가 확실합니다.
Joe Phillips

4
다음은 예입니다. 호출 프로그램은 37 바이트를 할당하도록 요청합니다. 라이브러리 함수는 40 바이트 이상의 블록을 찾습니다. 사용 가능리스트의 첫 번째 블록은 16 바이트입니다. 사용 가능리스트의 두 번째 블록은 12 바이트입니다. 세 번째 블록은 44 바이트입니다. 라이브러리는 그 시점에서 검색을 중지합니다.
Windows 프로그래머

6

힙 할당에 비해 수십 배의 성능 이점 외에도, 장기 할당 서버 애플리케이션에는 스택 할당이 바람직합니다. 최상의 관리되는 힙조차도 결국 조각화되어 응용 프로그램 성능이 저하됩니다.


4

스택은 용량이 제한되어 있지만 힙은 그렇지 않습니다. 프로세스 또는 스레드의 일반적인 스택은 약 8K입니다. 할당 된 크기는 변경할 수 없습니다.

스택 변수는 범위 지정 규칙을 따르지만 힙은 그렇지 않습니다. 명령어 포인터가 함수를 넘어 서면 함수와 관련된 모든 새 변수가 사라집니다.

무엇보다도 중요한 것은 전체 함수 호출 체인을 미리 예측할 수 없다는 것입니다. 따라서 사용자가 200 바이트 만 할당하면 스택 오버플로가 발생할 수 있습니다. 응용 프로그램이 아닌 라이브러리를 작성하는 경우 특히 중요합니다.


1
최신 OS에서 사용자 모드 스택에 할당 된 가상 주소 공간은 기본적으로 64kB 이상 (Windows의 경우 1MB) 이상일 수 있습니다. 커널 스택 크기에 대해 이야기하고 있습니까?
bk1e

1
내 컴퓨터에서 프로세스의 기본 스택 크기는 kB가 아닌 8MB입니다. 컴퓨터는 몇 살입니까?
Greg Rogers

3

나는 인생이 결정적이라고 생각하며 할당되는 것이 복잡한 방식으로 구성되어야하는지 생각합니다. 예를 들어, 트랜잭션 중심 모델링에서는 일반적으로 필드가 많은 트랜잭션 구조를 작성하고 조작 함수에 전달해야합니다. 예제는 OSCI SystemC TLM-2.0 표준을 참조하십시오.

작업 호출에 가까운 스택에이를 할당하면 구성이 비싸기 때문에 엄청난 오버 헤드가 발생하는 경향이 있습니다. 좋은 방법은 풀에 할당하거나 "이 모듈에는 하나의 트랜잭션 오브젝트 만 필요합니다"와 같은 간단한 정책을 사용하여 트랜잭션 오브젝트를 재사용하는 것입니다.

이는 각 작업 호출에서 객체를 할당하는 것보다 몇 배 더 빠릅니다.

그 이유는 대상이 값 비싼 구성과 상당히 긴 수명을 갖기 때문입니다.

나는 말할 것입니다 : 코드의 행동에 실제로 의존 할 수 있기 때문에 두 가지를 모두 시도하고 귀하의 경우에 가장 적합한 것을보십시오.


3

아마도 힙 할당 대 스택 할당의 가장 큰 문제는 일반적으로 힙 할당이 무제한 작업이므로 타이밍이 문제가되는 곳에서는 사용할 수 없다는 것입니다.

타이밍이 문제가되지 않는 다른 응용 프로그램의 경우 그다지 중요하지 않을 수 있지만 힙을 많이 할당하면 실행 속도에 영향을줍니다. 항상 짧은 수명과 자주 할당되는 메모리 (예 : 루프)에 스택을 사용하고 가능한 한 응용 프로그램 시작 중에 힙 할당을 시도하십시오.


3

스택 할당이 더 빠르지는 않습니다. 스택 변수를 사용하면 많은 승리를 거둘 수 있습니다. 그들은 더 나은 참조 지역을 가지고 있습니다. 마지막으로 할당 해제도 훨씬 저렴합니다.


3

스택 할당은 몇 개의 명령어이지만, 가장 빠른 rtos 힙 할당 자 (TLSF)는 150 개의 명령어 순서로 평균적으로 사용합니다. 또한 스택 할당에는 스레드 로컬 스토리지를 사용하기 때문에 잠금이 필요하지 않습니다. 따라서 환경이 얼마나 많이 멀티 스레딩되는지에 따라 스택 할당이 2-3 배 더 빠를 수 있습니다.

일반적으로 성능에 관심이있는 경우 힙 할당이 최후의 수단입니다. 중간에 실행 가능한 옵션은 고정 된 풀 할당 자일 수 있으며, 이는 몇 개의 명령 일 뿐이며 할당 당 오버 헤드가 거의 없으므로 작은 고정 크기 개체에 적합합니다. 단점은 고정 크기 객체에서만 작동하며 본질적으로 스레드 안전이 아니며 블록 조각화 문제가 있습니다.


3

C ++ 언어와 관련된 문제

우선, C ++에서 요구하는 소위 "스택"또는 "힙"할당이 없습니다 . 블록 범위의 자동 객체에 대해 이야기하는 경우 "할당"되지도 않습니다. (BTW, C의 자동 저장 기간은 확실히 "할당 된"과 동일 하지 않으며 , 후자는 C ++ 용어에서 "동적"입니다.) 동적으로 할당 된 메모리는 반드시 "힙"이 아니라 빈 저장소 에 있습니다. 후자는 종종 (기본) 구현입니다 입니다.

당으로하지만 추상 기계 의미 규칙, 자동 객체가 여전히 메모리를 차지, 순응 C ++ 구현 (이 프로그램의 관찰 동작을 변경하지 않는 경우)이이 문제가되지 않습니다 증명할 수있는 경우이 사실을 무시하도록 허용된다. 이 권한은 ISO C ++ 의 as-if 규칙 에 의해 부여되며 , 이는 일반적인 최적화를 가능하게하는 일반적인 조항이기도합니다 (또한 ISO C에도 거의 동일한 규칙이 있습니다). as-if 규칙 외에도 ISO C ++에는 복사 제거 규칙이 있습니다.특정 객체 생성을 생략 할 수 있습니다. 따라서 생성자와 소멸자 호출이 생략됩니다. 결과적으로 소스 코드에 의해 암시 된 순진한 추상 시맨틱과 비교하여 이러한 생성자와 소멸자의 자동 객체 (있는 경우)도 제거됩니다.

반면, 무료 매장 할당은 설계 상 "할당"입니다. ISO C ++ 규칙에서 이러한 할당은 할당 함수를 호출하여 수행 할 수 있습니다 . 그러나 ISO C ++ 14부터는 특정 경우에 전역 할당 함수 (예 :) 호출을 병합 할 수 있는 새로운 (있는 경우가 아닌) 규칙::operator new있습니다. 따라서 자동 할당의 경우와 같이 동적 할당 작업의 일부도 작동하지 않을 수 있습니다.

할당 기능은 메모리 자원을 할당합니다. 할당자를 사용한 할당에 따라 객체를 추가로 할당 할 수 있습니다. 자동 객체의 경우 기본 메모리에 액세스하여 다른 객체에 메모리를 제공하는 데 사용할 수 new있지만 (배치하여 ), 무료 저장소로 큰 의미가 없습니다. 다른 곳의 자원.

다른 모든 문제는 C ++의 범위를 벗어납니다. 그럼에도 불구하고 여전히 중요 할 수 있습니다.

C ++ 구현 정보

C ++은 통합 된 활성화 레코드 나 일종의 일류 연속체 (예 : 유명한 것 call/cc)를 노출하지 않으므로 구현시 자동 객체를 배치해야하는 활성화 레코드 프레임을 직접 조작 할 방법이 없습니다. 기본 구현 (인라인 어셈블리 코드와 같은 "기본"비 이식 가능 코드)과 (휴대 불가) 상호 운용이 없으면 프레임의 기본 할당을 생략하는 것이 매우 간단 할 수 있습니다. 예를 들어, 호출 된 함수가 인라인되면 프레임을 다른 프레임에 효과적으로 병합 할 수 있으므로 "할당"이 무엇인지 표시 할 방법이 없습니다.

그러나 일단 interops가 존중되면 상황이 복잡해지고 있습니다. C ++의 일반적인 구현은 네이티브 (ISA- 레벨 머신) 코드와 공유되는 이진 경계로서 일부 호출 규칙 을 사용하여 ISA (명령 세트 아키텍처)에서 interop의 기능을 노출합니다 . 스택 포인터를 유지할 때 특히 비용이 많이 들며 , 이는 종종 ISA 수준 레지스터에 의해 직접 유지됩니다 (종종 특정 기계 명령에 액세스해야 함). 스택 포인터는 (현재 활성화 된) 함수 호출의 상단 프레임 경계를 나타냅니다. 함수 호출이 입력되면 새 프레임이 필요하고 스택 포인터는 필요한 프레임 크기 이상의 값으로 (ISA 규칙에 따라) 더하거나 뺍니다. 그런 다음 프레임이 할당됩니다작업 후 스택 포인터. 함수에 대한 매개 변수는 호출에 사용 된 호출 규칙에 따라 스택 프레임으로 전달 될 수도 있습니다. 프레임은 C ++ 소스 코드에 의해 지정된 자동 객체 (아마도 매개 변수 포함)의 메모리를 보유 할 수 있습니다. 이러한 구현의 의미에서, 이러한 객체는 "할당"됩니다. 컨트롤이 함수 호출을 종료하면 더 이상 프레임이 필요하지 않습니다. 일반적으로 스택 포인터를 호출 이전의 상태로 다시 복원하여 호출합니다 (이전에 호출 규칙에 따라 저장 됨). 이것은 "할당 해제"로 볼 수 있습니다. 이러한 작업은 활성화 레코드를 LIFO 데이터 구조로 효과적으로 만들므로 종종 " (호출) 스택 "이라고합니다.

대부분의 C ++ 구현 (특히 ISA 수준 기본 코드를 대상으로하고 어셈블리 언어를 즉시 출력으로 사용하는 구현)은 이와 유사한 전략을 사용하므로 혼동되는 "할당"체계가 널리 사용됩니다. 이러한 할당 (및 할당 해제)은 머신 사이클을 소비하며, (최적화되지 않은) 호출이 자주 발생할 때 비용이 많이들 수 있지만, 최신 CPU 마이크로 아키텍처는 일반적인 코드 패턴 (예 : 구현 / 지침 에 스택 엔진 ).PUSHPOP

어쨌든, 일반적으로 는 스택 프레임 할당의 비용이 (가 완전히 떨어져 최적화는 제외) 무료 저장소를 운영 할당 함수를 호출보다 훨씬 적은 것이 사실이다 의 수백 (그렇지 않으면 수백만을 가질 수있는 자체 :-) 스택 포인터 및 기타 상태를 유지하기위한 작업. 할당 기능은 일반적으로 호스팅 된 환경에서 제공하는 API (예 : OS에서 제공하는 런타임)를 기반으로합니다. 함수 호출을 위해 자동 객체를 유지하는 목적과는 달리 이러한 할당은 범용이므로 스택과 같은 프레임 구조를 갖지 않습니다. 일반적으로 풀 스토리지에서 (또는 여러 힙) 이라는 공간을 할당합니다 . "스택"과 달리 여기서 "힙"개념은 사용중인 데이터 구조를 나타내지 않습니다.그것은 수십 년 전에 초기 언어 구현에서 비롯된 것입니다 . (BTW, 호출 스택은 일반적으로 프로그램 또는 스레드 시작 환경에서 힙에 의해 고정 또는 사용자 지정 크기로 할당됩니다.) 사용 사례의 특성상 힙에서의 할당 및 할당 해제가 푸시 또는 팝보다 훨씬 복잡합니다. 스택 프레임) 및 하드웨어에 의해 직접 최적화되는 것은 거의 불가능합니다.

메모리 액세스에 대한 영향

일반적인 스택 할당은 항상 새 프레임을 맨 위에 배치하므로 상당히 좋은 지역성을 갖습니다. 이것은 캐시에 친숙합니다. OTOH, 무료 저장소에 무작위로 할당 된 메모리에는 그러한 속성이 없습니다. ISO C ++ 17부터는에서 제공하는 풀 리소스 템플릿이 있습니다 <memory>. 이러한 인터페이스의 직접적인 목적은 연속 할당 결과가 메모리에서 서로 가깝게되도록하는 것입니다. 이는이 전략이 일반적으로 최신 구현에서 성능에 적합하다는 사실을 인정합니다. 그러나 이것은 할당 보다는 액세스 성능에 관한 것 입니다.

동시성

메모리에 대한 동시 액세스 기대는 스택과 힙간에 다른 영향을 미칠 수 있습니다. 호출 스택은 일반적으로 C ++ 구현에서 하나의 실행 스레드에 의해 독점적으로 소유됩니다. OTOH, 힙은 종종 프로세스의 스레드간에 공유 됩니다. 이러한 힙의 경우 할당 및 할당 해제 기능은 공유 내부 관리 데이터 구조를 데이터 경쟁으로부터 보호해야합니다. 결과적으로 힙 할당 및 할당 해제에는 내부 동기화 작업으로 인해 추가 오버 헤드가있을 수 있습니다.

공간 효율

사용 사례 및 내부 데이터 구조의 특성으로 인해 힙은 내부 메모리 조각화로 인해 어려움을 겪을 수 있지만 스택은 그렇지 않습니다. 이는 메모리 할당 성능에 직접적인 영향을 미치지 않지만 가상 메모리 가있는 시스템 에서 낮은 공간 효율성으로 인해 메모리 액세스의 전체 성능이 저하 될 수 있습니다. HDD가 실제 메모리의 스왑으로 사용될 때 특히 끔찍합니다. 대기 시간이 길어질 수 있으며 때로는 수십억 번의주기가 발생할 수 있습니다.

스택 할당의 한계

실제로 스택 할당이 힙 할당보다 성능이 우수하지만 스택 할당이 항상 힙 할당을 대체 할 수있는 것은 아닙니다.

첫째, ISO C ++를 사용하여 휴대용 방식으로 런타임에 지정된 크기로 스택에 공간을 할당 할 수있는 방법이 없습니다. allocaG ++의 VLA (가변 길이 배열)와 같은 구현에서 제공하는 확장이 있지만이를 피해야 할 이유가 있습니다. (IIRC, Linux 소스는 최근에 VLA 사용을 제거합니다.) 또한 ISO C99는 VLA를 의무화했지만 ISO C11은 지원을 옵션으로 설정합니다.

둘째, 스택 공간 소모를 감지 할 수있는 안정적이고 휴대 가능한 방법은 없습니다. 이것을 종종 스택 오버플로 (hmm,이 사이트의 어원)라고 하지만,보다 정확하게는 스택 오버런이라고 할 수 있습니다. 실제로, 이것은 종종 유효하지 않은 메모리 액세스를 유발하고 프로그램의 상태가 손상됩니다 (또는 아마도 보안 허점). 실제로 ISO C ++에는 "스택"이라는 개념이 없으며 리소스가 소진 될 때 정의되지 않은 동작을합니다 . 자동 물체를 위해 얼마나 많은 공간을 남겨 두어야하는지주의하십시오.

스택 공간이 부족하면 스택에 할당 된 객체가 너무 많아서 너무 많은 함수 호출 또는 자동 객체의 부적절한 사용으로 인해 발생할 수 있습니다. 이러한 경우에는 올바른 종료 조건이없는 재귀 함수 호출과 같은 버그가있을 수 있습니다.

그럼에도 불구하고 때로는 깊은 재귀 호출이 필요합니다. 언 바운드 활성 호출 (통화 깊이가 총 메모리에 의해서만 제한됨)을 지원해야하는 언어 구현에서는 일반적인 C ++ 구현과 같은 대상 언어 활성화 레코드로 (현대) 기본 호출 스택을 직접 사용할 수 없습니다 . 이 문제를 해결하려면 활성화 레코드를 구성하는 대체 방법이 필요합니다. 예를 들어, SML / NJ는 명시 적으로 힙에 프레임을 할당하고 선인장 스택을 사용합니다 . 이러한 활성화 레코드 프레임의 복잡한 할당은 일반적으로 호출 스택 프레임만큼 빠르지 않습니다. 그러나 그러한 언어가 적절한 꼬리 재귀를, 객체 언어의 직접 스택 할당 (즉, 언어의 "객체"는 참조로 저장되지 않지만 공유되지 않은 C ++ 객체에 일대일로 매핑 될 수있는 기본 프리미티브 값)은 더 복잡합니다. 일반적으로 성능 저하. C ++를 사용하여 이러한 언어를 구현할 때는 성능 영향을 추정하기가 어렵습니다.


stl과 마찬가지로 점점 더 적은 수의 사람들이 이러한 개념을 기꺼이 공유합니다. cppcon2018의 많은 친구들도 heap자주 사용 합니다.
陳 力

@ 陳 力 "The heap"은 특정 구현을 염두에두고 모호 할 수 있으므로 때로는 괜찮을 수도 있습니다. 그러나 "일반적으로"중복됩니다.
FrankHB

interop 란 무엇입니까?
陳 力

@ 陳 力 저는 C ++ 소스와 관련된 "원시"코드 상호 운용 (예 : 인라인 어셈블리 코드)을 의미했습니다. 이것은 C ++에서 다루지 않은 가정 (ABI의 가정)에 의존합니다. COM interop (일부 Windows 특정 ABI 기반)은 C ++에 중립적이기는하지만 다소 유사합니다.
FrankHB

2

이러한 최적화에 대한 일반적인 사항이 있습니다.

사용자가 얻는 최적화는 프로그램 카운터가 실제로 해당 코드에있는 시간에 비례합니다.

프로그램 카운터를 샘플링하면 시간을 소비하는 위치를 알 수 있습니다.이 코드는 일반적으로 코드의 작은 부분에 있으며 종종 라이브러리 루틴에서 제어 할 수 없습니다.

객체의 힙 할당에 많은 시간을 소비하는 경우에만 스택 할당 속도가 눈에 띄게 빨라집니다.


2

힙 할당자가 스택 기반 할당 기술을 단순히 사용할 수는 있지만 스택 할당은 거의 항상 힙 할당보다 빠르거나 빠릅니다.

그러나 스택 대 힙 기반 할당 (또는 로컬 대 외부 할당)의 전반적인 성능을 처리 할 때 더 큰 문제가 있습니다. 일반적으로 힙 (외부) 할당은 다양한 종류의 할당 및 할당 패턴을 처리하기 때문에 느립니다. 사용중인 할당 기의 범위를 줄이면 (알고리즘 / 코드에 로컬로 지정) 큰 변경없이 성능이 향상되는 경향이 있습니다. 예를 들어, 할당 및 할당 해제 쌍에 대한 LIFO 순서를 강제하는 등 할당 패턴에 더 나은 구조를 추가하면 할당자를보다 단순하고 구조화 된 방식으로 사용하여 할당 자의 성능을 향상시킬 수 있습니다. 또는 특정 할당 패턴에 맞게 조정 된 할당자를 사용하거나 작성할 수 있습니다. 대부분의 프로그램은 몇 개의 개별 크기를 자주 할당합니다. 따라서 고정 된 (바람직하게는 알려진) 크기의 lookaside 버퍼를 기반으로하는 힙은 매우 잘 수행됩니다. Windows는 바로 이런 이유로 조각화가 적은 힙을 사용합니다.

반면에, 스레드가 너무 많으면 32 비트 메모리 범위의 스택 기반 할당도 위험에 처하게됩니다. 스택에는 연속적인 메모리 범위가 필요하므로 스레드가 많을수록 스택 오버플로없이 실행하는 데 필요한 가상 주소 공간이 늘어납니다. 이것은 64 비트에서는 현재로서는 문제가되지 않지만 많은 스레드가있는 장기 실행 프로그램에서 혼란을 초래할 수 있습니다. 조각화로 인해 가상 주소 공간이 부족하면 항상 처리하기가 어렵습니다.


첫 문장에 동의하지 않습니다.
brian beuning

2

다른 사람들이 말했듯이 스택 할당은 일반적으로 훨씬 빠릅니다.

그러나 개체를 복사하는 데 비용이 많이 드는 경우 스택에 할당하면 나중에주의하지 않으면 개체를 사용할 때 성능이 크게 저하 될 수 있습니다.

예를 들어, 스택에 무언가를 할당 한 다음 컨테이너에 넣으면 힙에 할당하고 포인터를 컨테이너에 저장하는 것이 좋습니다 (예 : std :: shared_ptr <>). 값 및 기타 유사한 시나리오로 객체를 전달하거나 반환하는 경우에도 마찬가지입니다.

요점은 많은 경우 스택 할당이 일반적으로 힙 할당보다 낫지 만 때로는 계산 모델에 가장 적합하지 않을 때 스택 할당 방식을 벗어나면 해결하는 것보다 더 많은 문제가 발생할 수 있다는 것입니다.


2
class Foo {
public:
    Foo(int a) {

    }
}
int func() {
    int a1, a2;
    std::cin >> a1;
    std::cin >> a2;

    Foo f1(a1);
    __asm push a1;
    __asm lea ecx, [this];
    __asm call Foo::Foo(int);

    Foo* f2 = new Foo(a2);
    __asm push sizeof(Foo);
    __asm call operator new;//there's a lot instruction here(depends on system)
    __asm push a2;
    __asm call Foo::Foo(int);

    delete f2;
}

asm에서 이와 같이 될 것입니다. 당신이있을 때 funcf1포인터는 f2스택 (자동 저장)에 할당하고있다. 그리고 Foo f1(a1)는 스택 포인터에 명령 효과가 없습니다 (esp ) , 할당 된 func멤버 f1를 원한다면 명령은 다음과 같습니다 lea ecx [ebp+f1], call Foo::SomeFunc(). 스택이 할당 또 다른 것은 누군가가 메모리가 같은 것을 생각 할 수 FIFO, FIFO당신은 당신이 함수에있는 경우, 일부 기능에 가서 같이 뭔가를 할당 할 때 단지 일어난 int i = 0더 푸시가 일어나지 않았다.


1

스택 할당은 단순히 스택 포인터, 즉 대부분의 아키텍처에서 단일 명령을 이동시키는 것으로 언급되었습니다. 일반적으로 무엇을 비교힙 할당의 경우 발생 .

운영 체제는 사용 가능한 메모리의 일부를 사용 가능한 부분의 시작 주소에 대한 포인터와 사용 가능한 부분의 크기로 구성된 페이로드 데이터와 연결된 목록으로 유지합니다. X 바이트의 메모리를 할당하기 위해 링크 목록을 순회하고 각 메모를 순서대로 방문하여 크기가 X 이상인지 확인합니다. 크기가 P> = X 인 부분이 발견되면 P는 두 부분으로 분할됩니다. X와 PX 크기. 연결된 목록이 업데이트되고 첫 번째 부분에 대한 포인터가 반환됩니다.

보시다시피 힙 할당은 요청하는 메모리의 양, 메모리의 조각화 등의 요인에 따라 달라질 수 있습니다.


1

일반적으로 스택 할당은 위의 거의 모든 답변에서 언급했듯이 힙 할당보다 빠릅니다. 스택 푸시 또는 팝은 O (1) 인 반면 힙을 할당하거나 해제하면 이전 할당이 필요합니다. 그러나 일반적으로 성능이 집약적 인 꽉 조이는 루프에 할당해서는 안되므로 일반적으로 다른 요소가 선택됩니다.

이 구분을하는 것이 좋을 수도 있습니다. 힙에 "스택 할당 자"를 사용할 수 있습니다. 엄밀히 말하면, 할당의 위치가 아닌 실제 할당 방법을 의미하기 위해 스택 할당을 사용합니다. 실제 프로그램 스택에 많은 것을 할당하는 경우 여러 가지 이유로 나쁠 수 있습니다. 반면, 가능한 경우 스택 방법을 사용하여 힙에 할당하는 것이 할당 방법에 대해 최선의 선택입니다.

Metrowerks와 PPC를 언급 했으므로 Wii를 의미한다고 생각합니다. 이 경우 메모리는 매우 중요하며 가능한 한 스택 할당 방법을 사용하면 조각에서 메모리를 낭비하지 않습니다. 물론 이렇게하려면 "일반"힙 할당 방법보다 훨씬 더 많은주의가 필요합니다. 각 상황에 대한 장단점을 평가하는 것이 현명합니다.


1

스택 대 힙 할당을 선택할 때 고려해야 할 사항은 일반적으로 속도와 성능에 관한 것이 아닙니다. 스택은 스택처럼 작동하므로 블록을 밀고 다시 처음으로 터지는 데 적합합니다. 프로 시저 실행은 스택과 유사하며 마지막으로 입력 한 프로 시저가 먼저 종료됩니다. 대부분의 프로그래밍 언어에서 프로 시저에 필요한 모든 변수는 프로 시저 실행 중에 만 표시되므로 프로 시저를 시작할 때 푸시되고 종료 또는 리턴시 스택에서 튀어 나옵니다.

이제 스택을 사용할 수없는 예는 다음과 같습니다.

Proc P
{
  pointer x;
  Proc S
  {
    pointer y;
    y = allocate_some_data();
    x = y;
  }
}

프로 시저 S에서 일부 메모리를 할당하고 스택에 넣은 다음 S를 종료하면 할당 된 데이터가 스택에서 튀어 나옵니다. 그러나 P의 변수 x도 해당 데이터를 가리 키므로 x는 이제 알 수없는 내용으로 스택 포인터 아래의 특정 위치를 가리키고 있습니다 (스택이 아래로 자라고 가정). 스택 포인터가 그 아래의 데이터를 지우지 않고 위로 올라가면 내용이 여전히있을 수 있지만 스택에서 새 데이터 할당을 시작하면 포인터 x가 실제로 새 데이터를 가리킬 수 있습니다.


0

다른 응용 프로그램 코드 및 사용법이 기능에 영향을 줄 수 있으므로 조기에 가정하지 마십시오. 따라서 기능을 살펴보면 격리는 쓸모가 없습니다.

응용 프로그램이 심각한 경우 VTune을 사용하거나 유사한 프로파일 링 도구를 사용하고 핫스팟을 살펴보십시오.

케탄


-1

실제로 GCC에 의해 코드 생성 (VS도 기억) 스택 할당을 수행하는 데 오버 헤드가 없다고 말하고 싶습니다. .

다음 기능을 말하십시오.

  int f(int i)
  {
      if (i > 0)
      {   
          int array[1000];
      }   
  }

다음은 코드 생성입니다.

  __Z1fi:
  Leh_func_begin1:
      pushq   %rbp
  Ltmp0:
      movq    %rsp, %rbp
  Ltmp1:
      subq    $**3880**, %rsp <--- here we have the array allocated, even the if doesn't excited.
  Ltmp2:
      movl    %edi, -4(%rbp)
      movl    -8(%rbp), %eax
      addq    $3880, %rsp
      popq    %rbp
      ret 
  Leh_func_end1:

따라서 로컬 변수의 양에 관계없이 (if 또는 switch 내부에도) 3880 만 다른 값으로 변경됩니다. 로컬 변수가 없으면이 명령을 실행하면됩니다. 따라서 로컬 변수 할당에는 오버 헤드가 없습니다.

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