Cái nào nhanh hơn: Phân bổ ngăn xếp hoặc phân bổ Heap


503

Câu hỏi này nghe có vẻ khá cơ bản, nhưng đây là một cuộc tranh luận mà tôi đã có với một nhà phát triển khác mà tôi làm việc cùng.

Tôi đã quan tâm đến việc phân bổ những thứ mà tôi có thể, thay vì phân bổ chúng. Anh ấy đang nói chuyện với tôi và nhìn qua vai tôi và nhận xét rằng điều đó là không cần thiết bởi vì chúng là cùng một hiệu suất khôn ngoan.

Tôi luôn có ấn tượng rằng việc tăng ngăn xếp là thời gian không đổi và hiệu suất của phân bổ phụ thuộc vào độ phức tạp hiện tại của cả đống cho cả hai phân bổ (tìm một lỗ có kích thước phù hợp) và phân bổ (thu hẹp lỗ để giảm phân mảnh, như nhiều triển khai thư viện tiêu chuẩn cần có thời gian để thực hiện việc này trong quá trình xóa nếu tôi không nhầm).

Điều này gây ấn tượng với tôi như một cái gì đó có lẽ sẽ phụ thuộc rất nhiều vào trình biên dịch. Đối với dự án này, đặc biệt tôi đang sử dụng trình biên dịch Metrowerks cho kiến trúc PPC . Cái nhìn sâu sắc về sự kết hợp này sẽ hữu ích nhất, nhưng nói chung, đối với GCC và MSVC ++, trường hợp này là gì? Là phân bổ heap không hiệu suất cao như phân bổ ngăn xếp? Có sự khác biệt nào không? Hoặc là sự khác biệt để phút chốc nó trở thành tối ưu hóa vô nghĩa.


11
Tôi biết điều này khá cổ xưa, nhưng thật tuyệt khi thấy một số đoạn C / C ++ thể hiện các loại phân bổ khác nhau.
Joseph Weissman

42
Người chăn bò của bạn là người không biết gì ghê gớm, nhưng điều quan trọng hơn là anh ta nguy hiểm vì anh ta đưa ra những tuyên bố có thẩm quyền về những điều mà anh ta không biết gì về nó. Làm cho những người như vậy từ nhóm của bạn càng nhanh càng tốt.
Jim Balter

5
Lưu ý rằng các đống thường là nhiều hơn so với chồng. Nếu bạn được phân bổ số lượng lớn dữ liệu, bạn thực sự phải đưa nó vào heap, nếu không sẽ thay đổi kích thước ngăn xếp từ HĐH.
Paul Draper

1
Tất cả các tối ưu hóa là, trừ khi bạn có điểm chuẩn hoặc đối số phức tạp chứng minh khác, theo mặc định, tối ưu hóa vi mô vô nghĩa.
Bjorn Lindqvist

2
Tôi tự hỏi nếu đồng nghiệp của bạn có hầu hết kinh nghiệm Java hoặc C #. Trong các ngôn ngữ đó, gần như mọi thứ đều được phân bổ thành đống, điều này có thể dẫn đến các giả định đó.
Cort Ammon

Câu trả lời:


493

Phân bổ ngăn xếp nhanh hơn nhiều vì tất cả những gì nó thực sự làm là di chuyển con trỏ ngăn xếp. Sử dụng nhóm bộ nhớ, bạn có thể có được hiệu năng tương đương từ phân bổ heap, nhưng điều đó đi kèm với một sự phức tạp được thêm vào một chút và đau đầu của chính nó.

Ngoài ra, stack vs heap không chỉ là một xem xét hiệu suất; nó cũng cho bạn biết rất nhiều về tuổi thọ dự kiến ​​của các vật thể.


211
Và quan trọng hơn, stack luôn nóng, bộ nhớ bạn nhận được có nhiều khả năng nằm trong bộ nhớ cache hơn bất kỳ bộ nhớ được phân bổ heap xa nào
Benoît

47
Trên một số kiến ​​trúc (hầu hết được nhúng, mà tôi biết), ngăn xếp có thể được lưu trữ trong bộ nhớ chết nhanh (ví dụ SRAM). Điều này có thể tạo ra một sự khác biệt rất lớn!
leander

38
Bởi vì ngăn xếp thực sự là một ngăn xếp. Bạn không thể giải phóng một đoạn bộ nhớ được sử dụng bởi ngăn xếp trừ khi nó nằm trên đỉnh của nó. Không có quản lý, bạn đẩy hoặc bật những thứ trên đó. Mặt khác, bộ nhớ heap được quản lý: nó yêu cầu kernel cho các khối bộ nhớ, có thể chia tách chúng, hợp nhất chúng, tái sử dụng chúng và giải phóng chúng. Các ngăn xếp thực sự có nghĩa là để phân bổ nhanh và ngắn.
Benoît

24
@Pacerier Vì Stack nhỏ hơn Heap rất nhiều. Nếu bạn muốn phân bổ các mảng lớn, tốt hơn bạn nên phân bổ chúng trên Heap. Nếu bạn cố gắng phân bổ một mảng lớn trên Stack, nó sẽ cung cấp cho bạn một Stack Overflow. Hãy thử ví dụ trong C ++ này: int t [100000000]; Thử ví dụ t [10000000] = 10; và sau đó cout << t [10000000]; Nó sẽ cung cấp cho bạn một ngăn xếp tràn hoặc chỉ không hoạt động và sẽ không hiển thị cho bạn bất cứ điều gì. Nhưng nếu bạn phân bổ mảng trên heap: int * t = new int [100000000]; và thực hiện các hoạt động tương tự sau đó, nó sẽ hoạt động vì Heap có kích thước cần thiết cho một mảng lớn như vậy.
Lilian A. Moraru

7
@Pacerier Lý do rõ ràng nhất là các đối tượng trong ngăn xếp vượt ra khỏi phạm vi khi thoát khỏi khối mà chúng được phân bổ.
Jim Balter

166

Stack nhanh hơn nhiều. Nó thực sự chỉ sử dụng một hướng dẫn duy nhất trên hầu hết các kiến ​​trúc, trong hầu hết các trường hợp, ví dụ như trên x86:

sub esp, 0x10

(Điều đó di chuyển con trỏ ngăn xếp xuống 0x10 byte và do đó "phân bổ" các byte đó để sử dụng bởi một biến.)

Tất nhiên, kích thước của ngăn xếp là rất, rất hữu hạn, vì bạn sẽ nhanh chóng tìm ra nếu bạn lạm dụng phân bổ ngăn xếp hoặc cố gắng thực hiện đệ quy :-)

Ngoài ra, có rất ít lý do để tối ưu hóa hiệu suất của mã mà không thực sự cần nó, chẳng hạn như được thể hiện bằng cách định hình. "Tối ưu hóa sớm" thường gây ra nhiều vấn đề hơn giá trị của nó.

Nguyên tắc nhỏ của tôi: nếu tôi biết tôi sẽ cần một số dữ liệu vào thời gian biên dịch và nó có kích thước dưới vài trăm byte, tôi sẽ phân bổ nó. Nếu không thì tôi phân bổ nó.


20
Một hướng dẫn và thường được chia sẻ bởi TẤT CẢ các đối tượng trên ngăn xếp.
MSalters

9
Làm cho điểm tốt, đặc biệt là quan điểm về việc cần kiểm chứng nó. Tôi liên tục ngạc nhiên về sự lo lắng của mọi người về hiệu suất bị đặt sai chỗ.
Mike Dunlavey

6
"Giao dịch" cũng rất đơn giản và được thực hiện với một leavehướng dẫn duy nhất .
doc

15
Hãy ghi nhớ chi phí "ẩn" ở đây, đặc biệt là lần đầu tiên bạn mở rộng ngăn xếp. Làm như vậy có thể dẫn đến lỗi trang, chuyển đổi ngữ cảnh sang kernel cần thực hiện một số công việc để phân bổ bộ nhớ (hoặc tải nó từ trao đổi, trong trường hợp xấu nhất).
số

2
Trong một số trường hợp, bạn thậm chí có thể phân bổ nó với 0 hướng dẫn. Nếu một số thông tin được biết về số lượng byte cần được phân bổ, trình biên dịch có thể phân bổ chúng trước cùng lúc nó phân bổ các biến ngăn xếp khác. Trong những trường hợp đó, bạn không phải trả gì cả!
Cort Ammon

119

Thành thật mà nói, việc viết một chương trình để so sánh hiệu suất là chuyện nhỏ:

#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";
}

Người ta nói rằng một sự nhất quán ngu ngốc là hobgoblin của những tâm trí nhỏ . Rõ ràng tối ưu hóa trình biên dịch là sở thích của nhiều lập trình viên. Cuộc thảo luận này đã từng ở cuối câu trả lời, nhưng mọi người dường như không thể bận tâm đọc đến đó, vì vậy tôi sẽ chuyển nó lên đây để tránh nhận được câu hỏi mà tôi đã trả lời.

Một trình biên dịch tối ưu hóa có thể nhận thấy rằng mã này không làm gì cả và có thể tối ưu hóa tất cả. Công việc của trình tối ưu hóa là làm những việc như vậy và chiến đấu với trình tối ưu hóa là một việc vặt.

Tôi khuyên bạn nên biên dịch mã này với tối ưu hóa bị tắt vì không có cách nào tốt để đánh lừa mọi trình tối ưu hóa hiện đang sử dụng hoặc sẽ được sử dụng trong tương lai.

Bất cứ ai bật trình tối ưu hóa và sau đó phàn nàn về việc chiến đấu với nó sẽ phải chịu sự chế giễu công khai.

Nếu tôi quan tâm đến độ chính xác nano giây, tôi sẽ không sử dụng std::clock(). Nếu tôi muốn công bố kết quả như một luận án tiến sĩ, tôi sẽ làm cho một vấn đề lớn hơn về điều này, và tôi có thể sẽ so sánh GCC, Tendra / Ten15, LLVM, Watcom, Borland, Visual C ++, Digital Mars, ICC và các trình biên dịch khác. Như vậy, phân bổ heap mất nhiều thời gian hơn hàng trăm lần so với phân bổ ngăn xếp và tôi không thấy có gì hữu ích khi điều tra câu hỏi thêm nữa.

Trình tối ưu hóa có một nhiệm vụ để loại bỏ mã tôi đang thử nghiệm. Tôi không thấy bất kỳ lý do nào để bảo trình tối ưu hóa chạy và sau đó cố gắng đánh lừa trình tối ưu hóa để không thực sự tối ưu hóa. Nhưng nếu tôi thấy giá trị khi làm điều đó, tôi sẽ thực hiện một hoặc nhiều thao tác sau:

  1. Thêm một thành viên dữ liệu vào emptyvà truy cập thành viên dữ liệu đó trong vòng lặp; nhưng nếu tôi chỉ đọc từ thành viên dữ liệu thì trình tối ưu hóa có thể thực hiện gập và gỡ vòng lặp liên tục; nếu tôi chỉ ghi vào thành viên dữ liệu, trình tối ưu hóa có thể bỏ qua tất cả trừ lần lặp cuối cùng của vòng lặp. Ngoài ra, câu hỏi không phải là "phân bổ ngăn xếp và truy cập dữ liệu so với phân bổ heap và truy cập dữ liệu."

  2. Khai báo e volatile, nhưng volatilethường được biên dịch không chính xác (PDF).

  3. Lấy địa chỉ ebên trong vòng lặp (và có thể gán nó cho một biến được khai báo externvà định nghĩa trong tệp khác). Nhưng ngay cả trong trường hợp này, trình biên dịch có thể nhận thấy rằng - ít nhất là trên ngăn xếp - esẽ luôn được phân bổ tại cùng một địa chỉ bộ nhớ và sau đó thực hiện gập liên tục như trong (1) ở trên. Tôi nhận được tất cả các lần lặp của vòng lặp, nhưng đối tượng không bao giờ thực sự được phân bổ.

Ngoài ra, rõ ràng, bài kiểm tra này còn thiếu sót ở chỗ nó đo cả phân bổ và phân bổ, và câu hỏi ban đầu không hỏi về việc phân bổ. Tất nhiên các biến được phân bổ trên ngăn xếp sẽ tự động được phân bổ ở cuối phạm vi của chúng, vì vậy việc không gọi deletesẽ làm lệch các số (phân bổ ngăn xếp được bao gồm trong các số về phân bổ ngăn xếp, vì vậy chỉ công bằng khi đo mức phân bổ heap) và ( 2) gây rò rỉ bộ nhớ khá tệ, trừ khi chúng tôi giữ tham chiếu đến con trỏ mới và gọi deletesau khi chúng tôi đã đo thời gian.

Trên máy của tôi, sử dụng g ++ 3.4.4 trên Windows, tôi nhận được "0 đồng hồ tích tắc" cho cả phân bổ stack và heap cho bất kỳ phân bổ nào dưới 100000 và thậm chí sau đó tôi nhận được "0 tick đồng hồ" để phân bổ stack và "15 tick đồng hồ "Để phân bổ đống. Khi tôi đo 10.000.000 phân bổ, phân bổ ngăn xếp mất 31 tích tắc đồng hồ và phân bổ heap mất 1562 tích tắc đồng hồ.


Có, một trình biên dịch tối ưu hóa có thể giúp tạo ra các đối tượng trống. Nếu tôi hiểu chính xác, nó thậm chí có thể bỏ qua toàn bộ vòng lặp đầu tiên. Khi tôi tăng số lần lặp lên 10.000.000, phân bổ stack mất 31 tick đồng hồ và phân bổ heap mất 1562 tick đồng hồ. Tôi nghĩ thật an toàn khi nói rằng không cần nói với g ++ để tối ưu hóa khả năng thực thi, g ++ đã không bỏ qua các nhà xây dựng.


Trong những năm kể từ khi tôi viết bài này, ưu tiên trên Stack Overflow là đăng hiệu năng từ các bản dựng được tối ưu hóa. Nói chung, tôi nghĩ rằng điều này là chính xác. Tuy nhiên, tôi vẫn nghĩ thật ngớ ngẩn khi yêu cầu trình biên dịch tối ưu hóa mã khi bạn thực sự không muốn mã đó được tối ưu hóa. Nó gây ấn tượng với tôi vì rất giống với việc trả thêm tiền cho việc đỗ xe, nhưng từ chối giao chìa khóa. Trong trường hợp cụ thể này, tôi không muốn trình tối ưu hóa chạy.

Sử dụng một phiên bản điểm chuẩn được sửa đổi một chút (để giải quyết điểm hợp lệ mà chương trình ban đầu không phân bổ thứ gì đó trên ngăn xếp mỗi lần qua vòng lặp) và biên dịch mà không tối ưu hóa nhưng liên kết với thư viện phát hành (để giải quyết điểm hợp lệ mà chúng tôi không cung cấp 'không muốn bao gồm bất kỳ sự chậm trễ nào do liên kết đến các thư viện gỡ lỗi):

#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;
}

hiển thị:

on_stack took 2.070003 seconds
on_heap took 57.980081 seconds

trên hệ thống của tôi khi được biên dịch với dòng lệnh cl foo.cc /Od /MT /EHsc.

Bạn có thể không đồng ý với cách tiếp cận của tôi để có được bản dựng không được tối ưu hóa. Điều đó tốt: cảm thấy tự do sửa đổi điểm chuẩn nhiều như bạn muốn. Khi tôi bật tối ưu hóa, tôi nhận được:

on_stack took 0.000000 seconds
on_heap took 51.608723 seconds

Không phải vì phân bổ ngăn xếp thực sự là tức thời mà bởi vì bất kỳ trình biên dịch nửa vời nào cũng có thể nhận thấy rằng on_stackkhông làm gì hữu ích và có thể được tối ưu hóa. GCC trên máy tính xách tay Linux của tôi cũng thông báo rằng on_heapkhông làm gì hữu ích và cũng tối ưu hóa nó:

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds

2
Ngoài ra, bạn nên thêm một vòng lặp "hiệu chỉnh" ngay từ đầu chức năng chính của mình, một cái gì đó để cho bạn biết bạn cần bao nhiêu thời gian cho mỗi chu kỳ vòng lặp và điều chỉnh các vòng lặp khác để đảm bảo ví dụ của bạn chạy một số lượng thời gian, thay vì hằng số cố định bạn đang sử dụng.
Joe Pineda

2
Tôi cũng rất vui khi tăng số lần mỗi vòng lặp tùy chọn chạy (cộng với hướng dẫn g ++ không tối ưu hóa?) Đã mang lại kết quả đáng kể. Vì vậy, bây giờ chúng ta có sự thật khó khăn để nói stack là nhanh hơn. Cảm ơn những nỗ lực của bạn!
Joe Pineda

7
Đó là công việc của trình tối ưu hóa để loại bỏ mã như thế này. Có một lý do chính đáng để bật trình tối ưu hóa và sau đó ngăn không cho nó thực sự tối ưu hóa? Tôi đã chỉnh sửa câu trả lời để làm cho mọi thứ rõ ràng hơn: nếu bạn thích chiến đấu với trình tối ưu hóa, hãy chuẩn bị để tìm hiểu cách các nhà văn trình biên dịch thông minh.
Max Lybbert

3
Tôi rất muộn, nhưng cũng rất đáng nói ở đây là phân bổ heap yêu cầu bộ nhớ thông qua kernel, do đó, hiệu năng đạt được cũng phụ thuộc rất nhiều vào hiệu quả của kernel. Sử dụng mã này với Linux (Linux 3.10.7-gentoo # 2 SMP Thứ tư ngày 4 tháng 9 18:58:21 MDT 2013 x86_64), sửa đổi cho bộ đếm thời gian HR và sử dụng 100 triệu lần lặp trong mỗi vòng lặp mang lại hiệu suất này: stack allocation took 0.15354 seconds, heap allocation took 0.834044 secondsvới -O0bộ, thực hiện Phân bổ heap Linux chỉ chậm hơn trên hệ số khoảng 5,5 trên máy cụ thể của tôi.
Taywee

4
Trên các cửa sổ không có tối ưu hóa (xây dựng gỡ lỗi), nó sẽ sử dụng heap gỡ lỗi chậm hơn nhiều so với heap không gỡ lỗi. Tôi không nghĩ rằng đó là một ý tưởng tồi để "đánh lừa" trình tối ưu hóa cả. Trình biên dịch trình biên dịch là thông minh, nhưng trình biên dịch không phải là AI.
paulm

30

Một điều thú vị mà tôi đã tìm hiểu về Stack vs Heap Allocation trên bộ xử lý Xbox 360 Xenon, cũng có thể áp dụng cho các hệ thống đa lõi khác, đó là việc phân bổ trên Heap khiến Phần quan trọng bị dừng để chặn tất cả các lõi khác để phân bổ không xung đột. Do đó, trong một vòng lặp chặt chẽ, Stack Allocation là cách để đi đến các mảng có kích thước cố định vì nó ngăn chặn các quầy hàng.

Đây có thể là một cách tăng tốc khác để xem xét nếu bạn đang mã hóa cho đa lõi / đa nhân, trong đó phân bổ ngăn xếp của bạn sẽ chỉ có thể xem được bởi lõi chạy chức năng phạm vi của bạn và điều đó sẽ không ảnh hưởng đến bất kỳ lõi / CPU nào khác.


4
Điều đó đúng với hầu hết các máy đa lõi, không chỉ Xenon. Ngay cả Cell cũng phải làm điều đó bởi vì bạn có thể đang chạy hai luồng phần cứng trên lõi PPU đó.
Crashworks

15
Đó là một hiệu ứng của việc thực hiện (đặc biệt kém) của bộ cấp phát heap. Phân bổ heap tốt hơn không cần phải có được một khóa trên mỗi phân bổ.
Chris Dodd

19

Bạn có thể viết một bộ cấp phát heap đặc biệt cho các kích thước cụ thể của các đối tượng rất hiệu quả. Tuy nhiên, phân bổ heap chung không đặc biệt hiệu quả.

Ngoài ra tôi đồng ý với Torbjorn Gyllebring về tuổi thọ dự kiến ​​của các đối tượng. Điểm tốt!


1
Điều đó đôi khi được gọi là phân bổ phiến.
Benoit

8

Tôi không nghĩ phân bổ ngăn xếp và phân bổ heap thường có thể thay thế cho nhau. Tôi cũng hy vọng rằng hiệu suất của cả hai là đủ để sử dụng chung.

Tôi thực sự khuyên bạn nên cho các mục nhỏ, bất kỳ mục nào phù hợp hơn với phạm vi phân bổ. Đối với các mặt hàng lớn, đống có lẽ là cần thiết.

Trên các hệ điều hành 32 bit có nhiều luồng, ngăn xếp thường khá hạn chế (mặc dù thường ít nhất là vài mb), vì không gian địa chỉ cần được khắc lên và sớm hay muộn một ngăn xếp luồng sẽ chạy vào một luồng khác. Trên các hệ thống đơn luồng (dù sao Linux glibc cũng có luồng đơn), giới hạn ít hơn nhiều vì ngăn xếp chỉ có thể phát triển và tăng trưởng.

Trên các hệ điều hành 64 bit có đủ không gian địa chỉ để tạo các ngăn xếp luồng khá lớn.


6

Thông thường phân bổ ngăn xếp chỉ bao gồm trừ từ thanh ghi con trỏ ngăn xếp. Đây là tấn nhanh hơn so với tìm kiếm một đống.

Đôi khi phân bổ ngăn xếp yêu cầu thêm một trang bộ nhớ ảo. Thêm một trang mới của bộ nhớ zero không yêu cầu đọc một trang từ đĩa, do đó, việc này vẫn sẽ nhanh hơn rất nhiều so với tìm kiếm một đống (đặc biệt là nếu một phần của heap cũng được phân trang). Trong một tình huống hiếm hoi và bạn có thể xây dựng một ví dụ như vậy, đủ không gian chỉ có sẵn trong một phần của heap đã có trong RAM, nhưng việc phân bổ một trang mới cho ngăn xếp phải chờ một số trang khác được viết ra vào đĩa. Trong tình huống hiếm hoi đó, đống là nhanh hơn.


Tôi không nghĩ rằng đống được "tìm kiếm" trừ khi nó được phân trang. Khá chắc chắn bộ nhớ trạng thái rắn sử dụng bộ ghép kênh và có thể truy cập trực tiếp vào bộ nhớ, do đó Bộ nhớ truy cập ngẫu nhiên.
Joe Phillips

4
Đây là một ví dụ. Chương trình gọi yêu cầu phân bổ 37 byte. Hàm thư viện tìm kiếm một khối ít nhất 40 byte. Khối đầu tiên trong danh sách miễn phí có 16 byte. Khối thứ hai trong danh sách miễn phí có 12 byte. Khối thứ ba có 44 byte. Thư viện dừng tìm kiếm tại điểm đó.
Lập trình viên Windows

6

Ngoài lợi thế về hiệu suất của các đơn đặt hàng so với phân bổ heap, phân bổ ngăn xếp được ưu tiên hơn cho các ứng dụng máy chủ chạy dài. Ngay cả các đống được quản lý tốt nhất cuối cùng cũng bị phân mảnh đến mức hiệu năng ứng dụng xuống cấp.


4

Một ngăn xếp có khả năng hạn chế, trong khi một đống thì không. Ngăn xếp điển hình cho một quá trình hoặc luồng là khoảng 8K. Bạn không thể thay đổi kích thước một khi nó được phân bổ.

Một biến stack theo các quy tắc phạm vi, trong khi một heap không. Nếu con trỏ lệnh của bạn vượt ra ngoài một hàm, tất cả các biến mới được liên kết với hàm sẽ biến mất.

Quan trọng nhất trong tất cả, bạn không thể dự đoán trước chuỗi cuộc gọi chức năng tổng thể. Vì vậy, việc phân bổ 200 byte chỉ từ phía bạn có thể làm tăng tràn ngăn xếp. Điều này đặc biệt quan trọng nếu bạn đang viết thư viện, không phải ứng dụng.


1
Lượng không gian địa chỉ ảo được phân bổ cho ngăn xếp chế độ người dùng trên HĐH hiện đại có thể ít nhất là 64kB hoặc lớn hơn theo mặc định (1MB trên Windows). Bạn đang nói về kích thước ngăn xếp kernel?
bk1e

1
Trên máy của tôi, kích thước ngăn xếp mặc định cho một tiến trình là 8MB, không phải kB. Máy tính của bạn bao nhiêu tuổi?
Greg Rogers

3

Tôi nghĩ rằng cuộc đời là rất quan trọng, và liệu thứ được phân bổ có phải được xây dựng theo một cách phức tạp hay không. Ví dụ, trong mô hình hướng theo giao dịch, bạn thường phải điền và chuyển vào cấu trúc giao dịch với một loạt các trường cho các hàm hoạt động. Nhìn vào tiêu chuẩn OSCI SystemC TLM-2.0 để biết ví dụ.

Phân bổ những thứ này trên ngăn xếp gần với lời kêu gọi hoạt động có xu hướng gây ra chi phí rất lớn, vì việc xây dựng rất tốn kém. Cách tốt nhất là phân bổ trên heap và sử dụng lại các đối tượng giao dịch bằng cách gộp chung hoặc một chính sách đơn giản như "mô-đun này chỉ cần một đối tượng giao dịch".

Điều này nhanh hơn nhiều lần so với việc phân bổ đối tượng trên mỗi lệnh gọi hoạt động.

Lý do đơn giản là đối tượng có một công trình đắt tiền và thời gian sử dụng khá dài.

Tôi sẽ nói: hãy thử cả hai và xem những gì hoạt động tốt nhất trong trường hợp của bạn, bởi vì nó thực sự có thể phụ thuộc vào hành vi của mã của bạn.


3

Có lẽ vấn đề lớn nhất của phân bổ heap so với phân bổ stack, đó là phân bổ heap trong trường hợp chung là một hoạt động không giới hạn, và do đó bạn không thể sử dụng nó trong trường hợp thời gian là một vấn đề.

Đối với các ứng dụng khác trong đó thời gian không phải là vấn đề, nó có thể không quan trọng lắm, nhưng nếu bạn phân bổ nhiều, điều này sẽ ảnh hưởng đến tốc độ thực thi. Luôn cố gắng sử dụng ngăn xếp cho bộ nhớ trong thời gian ngắn và thường được phân bổ (ví dụ như trong các vòng lặp) và càng lâu càng tốt - thực hiện phân bổ heap trong khi khởi động ứng dụng.


3

Đó không phải là phân bổ ngăn xếp jsut mà nhanh hơn. Bạn cũng giành được rất nhiều khi sử dụng các biến stack. Họ có địa phương tốt hơn của tài liệu tham khảo. Và cuối cùng, thỏa thuận cũng rẻ hơn rất nhiều.


3

Phân bổ ngăn xếp là một vài hướng dẫn trong khi phân bổ heap rtos nhanh nhất mà tôi biết (TLSF) sử dụng trung bình theo thứ tự 150 lệnh. Ngoài ra phân bổ ngăn xếp không yêu cầu khóa vì họ sử dụng lưu trữ cục bộ luồng, đó là một chiến thắng hiệu suất lớn khác. Vì vậy, phân bổ ngăn xếp có thể nhanh hơn 2-3 bậc tùy thuộc vào mức độ đa luồng của môi trường của bạn.

Nói chung, phân bổ heap là giải pháp cuối cùng của bạn nếu bạn quan tâm đến hiệu suất. Tùy chọn ở giữa khả thi có thể là một công cụ phân bổ nhóm cố định, cũng chỉ là một vài hướng dẫn và có rất ít chi phí cho mỗi lần phân bổ, vì vậy nó rất phù hợp cho các đối tượng có kích thước cố định nhỏ. Mặt khác, nó chỉ hoạt động với các đối tượng có kích thước cố định, vốn không phải là chủ đề an toàn và có các vấn đề phân mảnh.


3

Mối quan tâm cụ thể đối với ngôn ngữ C ++

Trước hết, không có cái gọi là phân bổ "stack" hay "heap" được ủy quyền bởi C ++ . Nếu bạn đang nói về các đối tượng tự động trong phạm vi khối, chúng thậm chí không được "phân bổ". (BTW, thời gian lưu trữ tự động trong C chắc chắn là KHÔNG như vậy để "phân bổ", sau này là "năng động" trong C ++ cách nói.) Bộ nhớ cấp phát động là trên cửa hàng miễn phí , không nhất thiết phải vào "đống", mặc dù sau này thường là việc thực hiện (mặc định) .

Mặc dù theo quy tắc ngữ nghĩa của máy trừu tượng , các đối tượng tự động vẫn chiếm bộ nhớ, việc triển khai C ++ tuân thủ được phép bỏ qua thực tế này khi nó có thể chứng minh điều này không quan trọng (khi nó không thay đổi hành vi có thể quan sát được của chương trình). Quyền này được cấp bởi quy tắc as-if trong ISO C ++, đây cũng là điều khoản chung cho phép tối ưu hóa thông thường (và cũng có một quy tắc gần như tương tự trong ISO C). Bên cạnh quy tắc as-if, ISO C ++ cũng các quy tắc bầu chọn sao chépphải cho phép bỏ qua các sáng tạo cụ thể của các đối tượng. Do đó, các hàm gọi và hàm hủy có liên quan được bỏ qua. Kết quả là, các đối tượng tự động (nếu có) trong các hàm tạo và hàm hủy này cũng bị loại bỏ, so với ngữ nghĩa trừu tượng ngây thơ ngụ ý bởi mã nguồn.

Mặt khác, phân bổ cửa hàng miễn phí chắc chắn là "phân bổ" theo thiết kế. Theo quy tắc ISO C ++, việc phân bổ như vậy có thể đạt được bằng cách gọi hàm phân bổ . Tuy nhiên, kể từ ISO C ++ 14, có một quy tắc mới (không phải là nếu) để cho phép hợp nhất ::operator newcác lệnh gọi hàm phân bổ toàn cầu (nghĩa là ) trong các trường hợp cụ thể. Vì vậy, các bộ phận của hoạt động phân bổ động cũng có thể không hoạt động như trường hợp của các đối tượng tự động.

Các chức năng phân bổ phân bổ tài nguyên của bộ nhớ. Các đối tượng có thể được phân bổ thêm dựa trên phân bổ sử dụng phân bổ. Đối với các đối tượng tự động, chúng được trình bày trực tiếp - mặc dù bộ nhớ bên dưới có thể được truy cập và được sử dụng để cung cấp bộ nhớ cho các đối tượng khác (theo vị trí new), nhưng điều này không có ý nghĩa lớn như cửa hàng miễn phí, vì không có cách nào để di chuyển tài nguyên ở nơi khác.

Tất cả các mối quan tâm khác nằm ngoài phạm vi của C ++. Tuy nhiên, chúng có thể vẫn còn đáng kể.

Về triển khai C ++

C ++ không để lộ các bản ghi kích hoạt hợp nhất hoặc một số loại tiếp tục hạng nhất (ví dụ như nổi tiếng call/cc), không có cách nào để thao tác trực tiếp các khung bản ghi kích hoạt - nơi thực hiện cần đặt các đối tượng tự động. Khi không có sự tương tác (không di động) với triển khai cơ bản (mã không di động "gốc", chẳng hạn như mã lắp ráp nội tuyến), việc bỏ qua phân bổ cơ bản của các khung có thể khá nhỏ. Ví dụ, khi hàm được gọi được nội tuyến, các khung có thể được hợp nhất một cách hiệu quả với các hàm khác, vì vậy không có cách nào để hiển thị "phân bổ" là gì.

Tuy nhiên, một khi sự can thiệp được tôn trọng, mọi thứ đang trở nên phức tạp. Một triển khai điển hình của C ++ sẽ cho thấy khả năng can thiệp vào ISA (kiến trúc tập lệnh) với một số quy ước gọi là ranh giới nhị phân được chia sẻ với mã gốc (máy cấp độ ISA). Điều này sẽ rất tốn kém, đáng chú ý, khi duy trì con trỏ ngăn xếp , thường được giữ trực tiếp bởi một thanh ghi cấp độ ISA (với các hướng dẫn máy cụ thể có thể truy cập). Con trỏ ngăn xếp chỉ ra ranh giới của khung trên cùng của lệnh gọi hàm (hiện đang hoạt động). Khi một lệnh gọi hàm được nhập, một khung mới là cần thiết và con trỏ ngăn xếp được thêm hoặc bớt (tùy theo quy ước của ISA) bởi một giá trị không nhỏ hơn kích thước khung yêu cầu. Khung được phân bổkhi con trỏ ngăn xếp sau các hoạt động. Các tham số của các chức năng cũng có thể được truyền vào khung ngăn xếp, tùy thuộc vào quy ước gọi được sử dụng cho cuộc gọi. Khung có thể chứa bộ nhớ của các đối tượng tự động (có thể bao gồm các tham số) được chỉ định bởi mã nguồn C ++. Theo nghĩa thực hiện như vậy, các đối tượng này được "phân bổ". Khi điều khiển thoát khỏi lệnh gọi hàm, khung không còn cần thiết nữa, nó thường được giải phóng bằng cách khôi phục con trỏ ngăn xếp trở lại trạng thái trước cuộc gọi (được lưu trước đó theo quy ước gọi). Điều này có thể được xem là "thỏa thuận". Các hoạt động này làm cho bản ghi kích hoạt có hiệu quả cấu trúc dữ liệu LIFO, do đó, nó thường được gọi là " ngăn xếp (cuộc gọi) ".

Bởi vì hầu hết các triển khai C ++ (đặc biệt là các triển khai nhắm mục tiêu mã gốc cấp độ ISA và sử dụng ngôn ngữ hợp ngữ làm đầu ra ngay lập tức của nó) sử dụng các chiến lược tương tự như thế này, nên sơ đồ "phân bổ" khó hiểu là phổ biến. Việc phân bổ như vậy (cũng như thỏa thuận) làm chi tiêu chu kỳ máy và có thể tốn kém khi các cuộc gọi (không được tối ưu hóa) xảy ra thường xuyên, ngay cả khi các cấu trúc vi mô CPU hiện đại có thể được tối ưu hóa phức tạp được triển khai bởi phần cứng cho mẫu mã chung (như sử dụng công cụ ngăn xếp trong thực hiện PUSH/ POPhướng dẫn).

Nhưng dù sao, nói chung, sự thật là chi phí phân bổ khung ngăn xếp ít hơn đáng kể so với một cuộc gọi đến chức năng phân bổ vận hành cửa hàng miễn phí (trừ khi nó được tối ưu hóa hoàn toàn) , bản thân nó có thể có hàng trăm (nếu không phải là hàng triệu :-) hoạt động để duy trì con trỏ ngăn xếp và các trạng thái khác. Các chức năng phân bổ thường dựa trên API được cung cấp bởi môi trường được lưu trữ (ví dụ: thời gian chạy do HĐH cung cấp). Khác với mục đích giữ các đối tượng tự động cho các cuộc gọi chức năng, các phân bổ như vậy có mục đích chung, vì vậy chúng sẽ không có cấu trúc khung như một ngăn xếp. Theo truyền thống, họ phân bổ không gian từ kho lưu trữ gọi là heap (hoặc một vài đống). Khác với "ngăn xếp", khái niệm "heap" ở đây không chỉ ra cấu trúc dữ liệu đang được sử dụng;nó bắt nguồn từ việc thực hiện ngôn ngữ sớm từ nhiều thập kỷ trước . (BTW, ngăn xếp cuộc gọi thường được phân bổ với kích thước cố định hoặc do người dùng chỉ định từ vùng heap theo môi trường khi khởi động chương trình hoặc luồng.) Bản chất của các trường hợp sử dụng khiến việc phân bổ và giải quyết từ một đống phức tạp hơn nhiều so với đẩy hoặc bật ngăn xếp khung) và hầu như không thể được tối ưu hóa trực tiếp bằng phần cứng.

Hiệu ứng truy cập bộ nhớ

Phân bổ ngăn xếp thông thường luôn đặt khung mới lên hàng đầu, vì vậy nó có một địa phương khá tốt. Điều này là thân thiện với bộ nhớ cache. OTOH, bộ nhớ được phân bổ ngẫu nhiên trong cửa hàng miễn phí không có thuộc tính đó. Kể từ ISO C ++ 17, có các mẫu tài nguyên nhóm được cung cấp bởi <memory>. Mục đích trực tiếp của giao diện như vậy là cho phép kết quả phân bổ liên tiếp gần nhau trong bộ nhớ. Điều này thừa nhận thực tế rằng chiến lược này thường tốt cho hiệu suất với các triển khai hiện đại, ví dụ như thân thiện với bộ đệm trong các kiến ​​trúc hiện đại. Đây là về hiệu suất truy cập hơn là phân bổ , mặc dù.

Đồng thời

Kỳ vọng truy cập đồng thời vào bộ nhớ có thể có các hiệu ứng khác nhau giữa ngăn xếp và đống. Một ngăn xếp cuộc gọi thường được sở hữu độc quyền bởi một luồng thực thi trong triển khai C ++. OTOH, đống thường được chia sẻ giữa các luồng trong một quy trình. Đối với các đống như vậy, các hàm phân bổ và phân bổ phải bảo vệ cấu trúc dữ liệu quản trị nội bộ được chia sẻ khỏi cuộc đua dữ liệu. Do đó, phân bổ heap và thỏa thuận có thể có thêm chi phí do hoạt động đồng bộ hóa nội bộ.

Hiệu quả không gian

Do tính chất của các trường hợp sử dụng và cấu trúc dữ liệu bên trong, các đống có thể bị phân mảnh bộ nhớ trong, trong khi ngăn xếp thì không. Điều này không có tác động trực tiếp đến hiệu suất phân bổ bộ nhớ, nhưng trong một hệ thống có bộ nhớ ảo , hiệu suất không gian thấp có thể làm suy giảm hiệu suất tổng thể của việc truy cập bộ nhớ. Điều này đặc biệt khủng khiếp khi HDD được sử dụng như một sự trao đổi bộ nhớ vật lý. Nó có thể gây ra độ trễ khá dài - đôi khi là hàng tỷ chu kỳ.

Hạn chế của phân bổ ngăn xếp

Mặc dù phân bổ ngăn xếp thường có hiệu suất vượt trội so với phân bổ heap trong thực tế, nhưng chắc chắn không có nghĩa là phân bổ ngăn xếp luôn có thể thay thế phân bổ heap.

Đầu tiên, không có cách nào để phân bổ không gian trên ngăn xếp với kích thước được chỉ định trong thời gian chạy theo cách di động với ISO C ++. Có các phần mở rộng được cung cấp bởi các triển khai như allocavà VLA của G ++ (mảng có độ dài thay đổi), nhưng có những lý do để tránh chúng. (IIRC, nguồn Linux loại bỏ việc sử dụng VLA gần đây.) (Cũng lưu ý rằng ISO C99 không bắt buộc phải có VLA, nhưng ISO C11 biến tùy chọn hỗ trợ.)

Thứ hai, không có cách nào đáng tin cậy và di động để phát hiện cạn kiệt không gian ngăn xếp. Điều này thường được gọi là tràn ngăn xếp (hmm, từ nguyên của trang web này) , nhưng có lẽ chính xác hơn, chồng tràn . Trong thực tế, điều này thường gây ra truy cập bộ nhớ không hợp lệ và trạng thái của chương trình sau đó bị hỏng (... hoặc có thể tệ hơn là lỗ hổng bảo mật). Trên thực tế, ISO C ++ không có khái niệm về "ngăn xếp" và khiến nó không được xác định hành vi khi tài nguyên cạn kiệt . Hãy thận trọng về việc nên để lại bao nhiêu phòng cho các đối tượng tự động.

Nếu không gian ngăn xếp hết, có quá nhiều đối tượng được phân bổ trong ngăn xếp, điều này có thể được gây ra bởi quá nhiều lệnh gọi chức năng hoạt động hoặc sử dụng không đúng đối tượng tự động. Các trường hợp như vậy có thể gợi ý sự tồn tại của các lỗi, ví dụ như một hàm gọi đệ quy mà không có điều kiện thoát chính xác.

Tuy nhiên, các cuộc gọi đệ quy sâu đôi khi được mong muốn. Trong việc triển khai các ngôn ngữ yêu cầu hỗ trợ các cuộc gọi hoạt động không liên kết (trong đó độ sâu cuộc gọi chỉ bị giới hạn bởi tổng bộ nhớ), không thể sử dụng trực tiếp ngăn xếp cuộc gọi gốc (hiện đại) làm bản ghi kích hoạt ngôn ngữ đích như các triển khai C ++ thông thường. Để khắc phục sự cố, cần có các cách khác để xây dựng hồ sơ kích hoạt. Ví dụ, SML / NJ phân bổ rõ ràng các khung trên heap và sử dụng các ngăn xếp xương rồng . Việc phân bổ phức tạp các khung bản ghi kích hoạt như vậy thường không nhanh bằng các khung ngăn xếp cuộc gọi. Tuy nhiên, nếu các ngôn ngữ đó được triển khai hơn nữa với sự đảm bảo của đệ quy đuôi thích hợp, phân bổ ngăn xếp trực tiếp trong ngôn ngữ đối tượng (nghĩa là "đối tượng" trong ngôn ngữ không được lưu trữ dưới dạng tham chiếu, nhưng các giá trị nguyên thủy nguyên gốc có thể được ánh xạ một đến một đối tượng C ++ không được chia sẻ) thậm chí còn phức tạp hơn với thực hiện phạt chung. Khi sử dụng C ++ để thực hiện các ngôn ngữ như vậy, rất khó để ước tính các tác động hiệu suất.


Giống như stl, ngày càng ít hơn sẵn sàng để khác biệt các khái niệm này. Nhiều dudes trên cppcon2018 cũng sử dụng heapthường xuyên.

@ 陳 "Heap" có thể không rõ ràng với một số triển khai cụ thể được ghi nhớ, vì vậy đôi khi có thể OK. Nó là dư thừa "nói chung", mặc dù.
FrankHB

Interop là gì?

@ 陳 Tôi có nghĩa là bất kỳ loại tương tác mã "gốc" nào liên quan đến nguồn C ++, ví dụ, bất kỳ mã lắp ráp nội tuyến nào. Điều này phụ thuộc vào các giả định (của ABI) không được đề cập trong C ++. COM interop (dựa trên một số ABI dành riêng cho Windows) ít nhiều giống nhau, mặc dù nó chủ yếu là trung tính với C ++.
FrankHB

2

Có một điểm chung để được thực hiện về tối ưu hóa như vậy.

Tối ưu hóa bạn nhận được tỷ lệ thuận với thời gian bộ đếm chương trình thực sự nằm trong mã đó.

Nếu bạn lấy mẫu bộ đếm chương trình, bạn sẽ tìm ra nơi nó dành thời gian và đó thường là một phần nhỏ của mã và thường trong các thư viện thường xuyên bạn không kiểm soát được.

Chỉ khi bạn thấy nó tiêu tốn nhiều thời gian trong việc phân bổ heap của các đối tượng của bạn thì nó sẽ nhanh hơn đáng kể để phân bổ chúng.


2

Phân bổ ngăn xếp hầu như sẽ luôn nhanh hoặc nhanh hơn phân bổ heap, mặc dù việc phân bổ heap chỉ đơn giản là sử dụng kỹ thuật phân bổ dựa trên ngăn xếp.

Tuy nhiên, có những vấn đề lớn hơn khi xử lý hiệu suất tổng thể của stack so với phân bổ dựa trên heap (hoặc theo thuật ngữ tốt hơn một chút, phân bổ cục bộ so với phân bổ bên ngoài). Thông thường, phân bổ heap (bên ngoài) là chậm vì nó đang xử lý nhiều loại phân bổ và mẫu phân bổ khác nhau. Việc giảm phạm vi của công cụ cấp phát mà bạn đang sử dụng (biến nó thành cục bộ cho thuật toán / mã) sẽ có xu hướng tăng hiệu suất mà không có bất kỳ thay đổi lớn nào. Việc thêm cấu trúc tốt hơn vào các mẫu phân bổ của bạn, ví dụ, buộc một thứ tự LIFO trên các cặp phân bổ và phân bổ cũng có thể cải thiện hiệu suất của bộ cấp phát của bạn bằng cách sử dụng bộ cấp phát theo cách đơn giản và có cấu trúc hơn. Hoặc, bạn có thể sử dụng hoặc viết một bộ cấp phát được điều chỉnh cho mẫu phân bổ cụ thể của bạn; hầu hết các chương trình phân bổ một vài kích thước riêng biệt thường xuyên, do đó, một đống dựa trên bộ đệm lookaside của một vài kích thước cố định (tốt nhất là đã biết) sẽ hoạt động rất tốt. Windows sử dụng heap phân mảnh thấp của nó vì lý do này rất.

Mặt khác, phân bổ dựa trên ngăn xếp trên phạm vi bộ nhớ 32 bit cũng đầy rủi ro nếu bạn có quá nhiều luồng. Các ngăn xếp cần một phạm vi bộ nhớ liền kề, vì vậy bạn càng có nhiều luồng, bạn càng cần nhiều không gian địa chỉ ảo để chúng chạy mà không bị tràn ngăn xếp. Đây sẽ không phải là vấn đề (hiện tại) với 64 bit, nhưng chắc chắn nó có thể tàn phá các chương trình chạy dài với nhiều luồng. Hết không gian địa chỉ ảo do bị phân mảnh luôn là một vấn đề khó giải quyết.


Tôi không đồng ý với câu đầu tiên của bạn.
brian beuning

2

Như những người khác đã nói, phân bổ ngăn xếp thường nhanh hơn nhiều.

Tuy nhiên, nếu các đối tượng của bạn tốn kém để sao chép, việc phân bổ trên ngăn xếp có thể dẫn đến một hiệu suất lớn sau này khi bạn sử dụng các đối tượng nếu bạn không cẩn thận.

Ví dụ: nếu bạn phân bổ một cái gì đó trên ngăn xếp, và sau đó đặt nó vào một thùng chứa, tốt hơn là nên phân bổ trên heap và lưu trữ con trỏ trong vùng chứa (ví dụ: với std :: shared_ptr <>). Điều tương tự cũng đúng nếu bạn chuyển hoặc trả lại các đối tượng theo giá trị và các tình huống tương tự khác.

Vấn đề là mặc dù phân bổ ngăn xếp thường tốt hơn phân bổ heap trong nhiều trường hợp, đôi khi nếu bạn không theo cách phân bổ ngăn xếp khi nó không phù hợp nhất với mô hình tính toán, nó có thể gây ra nhiều vấn đề hơn giải quyết.


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;
}

Nó sẽ như thế này trong asm. Khi bạn vào func, f1con trỏ và f2đã được phân bổ trên ngăn xếp (lưu trữ tự động). Và nhân tiện, Foo f1(a1)không có hiệu ứng hướng dẫn trên con trỏ ngăn xếp ( esp), Nó đã được phân bổ, nếu funcmuốn có được thành viên f1, hướng dẫn của nó là như thế này : lea ecx [ebp+f1], call Foo::SomeFunc(). Một điều khác mà ngăn xếp ngăn xếp có thể khiến ai đó nghĩ rằng bộ nhớ là một thứ gì đó FIFO, điều FIFOvừa xảy ra khi bạn đi vào một số chức năng, nếu bạn đang ở trong chức năng và phân bổ một cái gì đó như thế int i = 0, không có sự thúc đẩy nào xảy ra.


1

Nó đã được đề cập trước khi phân bổ ngăn xếp chỉ đơn giản là di chuyển con trỏ ngăn xếp, nghĩa là, một lệnh duy nhất trên hầu hết các kiến ​​trúc. So sánh điều đó với những gì thường xảy ra trong trường hợp phân bổ heap.

Hệ điều hành duy trì các phần của bộ nhớ trống dưới dạng danh sách được liên kết với dữ liệu tải trọng bao gồm con trỏ đến địa chỉ bắt đầu của phần miễn phí và kích thước của phần miễn phí. Để phân bổ X byte bộ nhớ, danh sách liên kết được duyệt qua và mỗi ghi chú được truy cập theo trình tự, kiểm tra xem kích thước của nó có ít nhất là X. Khi tìm thấy một phần có kích thước P> = X, P được chia thành hai phần với kích thước X và PX. Danh sách liên kết được cập nhật và con trỏ đến phần đầu tiên được trả về.

Như bạn có thể thấy, phân bổ heap phụ thuộc vào các yếu tố có thể như dung lượng bộ nhớ bạn yêu cầu, bộ nhớ bị phân mảnh như thế nào.


1

Nói chung, phân bổ ngăn xếp nhanh hơn phân bổ heap như được đề cập bởi hầu hết mọi câu trả lời ở trên. Đẩy hoặc pop stack là O (1), trong khi phân bổ hoặc giải phóng khỏi một đống có thể yêu cầu đi bộ phân bổ trước đó. Tuy nhiên, bạn thường không nên phân bổ trong các vòng lặp chặt chẽ, hiệu suất cao, vì vậy sự lựa chọn thường sẽ đi xuống các yếu tố khác.

Có thể tốt để tạo sự khác biệt này: bạn có thể sử dụng "bộ cấp phát ngăn xếp" trên heap. Nói đúng ra, tôi lấy phân bổ ngăn xếp có nghĩa là phương pháp phân bổ thực tế hơn là vị trí phân bổ. Nếu bạn đang phân bổ nhiều thứ trên ngăn xếp chương trình thực tế, điều đó có thể tệ vì nhiều lý do. Mặt khác, sử dụng phương thức ngăn xếp để phân bổ trên heap khi có thể là lựa chọn tốt nhất bạn có thể thực hiện cho phương thức phân bổ.

Vì bạn đã đề cập đến Metrowerks và PPC, tôi đoán bạn có nghĩa là Wii. Trong trường hợp này, bộ nhớ ở mức cao và sử dụng phương pháp phân bổ ngăn xếp bất cứ khi nào có thể đảm bảo rằng bạn không lãng phí bộ nhớ vào các mảnh. Tất nhiên, làm điều này đòi hỏi nhiều sự chăm sóc hơn các phương pháp phân bổ heap "bình thường". Thật khôn ngoan khi đánh giá sự đánh đổi cho từng tình huống.


1

Lưu ý rằng các cân nhắc thường không phải là về tốc độ và hiệu suất khi chọn ngăn xếp so với phân bổ heap. Ngăn xếp hoạt động giống như một ngăn xếp, có nghĩa là nó rất phù hợp để đẩy các khối và bật lại chúng, lần cuối vào, ra trước. Việc thực hiện các thủ tục cũng giống như stack, thủ tục cuối cùng được nhập trước tiên sẽ được thoát. Trong hầu hết các ngôn ngữ lập trình, tất cả các biến cần thiết trong một thủ tục sẽ chỉ hiển thị trong quá trình thực thi thủ tục, do đó chúng được đẩy khi vào một thủ tục và bật ra khỏi ngăn xếp khi thoát hoặc trả về.

Bây giờ cho một ví dụ trong đó ngăn xếp không thể được sử dụng:

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

Nếu bạn cấp phát một số bộ nhớ trong thủ tục S và đặt nó vào ngăn xếp và sau đó thoát S, dữ liệu được phân bổ sẽ được bật ra khỏi ngăn xếp. Nhưng biến x trong P cũng chỉ vào dữ liệu đó, vì vậy x hiện đang trỏ đến một nơi nào đó bên dưới con trỏ ngăn xếp (giả sử ngăn xếp phát triển xuống dưới) với một nội dung không xác định. Nội dung có thể vẫn ở đó nếu con trỏ ngăn xếp chỉ được di chuyển lên mà không xóa dữ liệu bên dưới nó, nhưng nếu bạn bắt đầu phân bổ dữ liệu mới trên ngăn xếp, thì con trỏ x thực sự có thể trỏ đến dữ liệu mới đó.


0

Không bao giờ giả định sớm vì mã ứng dụng và việc sử dụng khác có thể ảnh hưởng đến chức năng của bạn. Vì vậy, nhìn vào chức năng là cô lập là không sử dụng.

Nếu bạn nghiêm túc với ứng dụng thì VTune nó hoặc sử dụng bất kỳ công cụ định hình tương tự nào và xem các điểm nóng.

Ketan


-1

Tôi muốn nói rằng mã thực sự được tạo bởi GCC (tôi cũng nhớ VS) không có chi phí để thực hiện phân bổ ngăn xếp .

Nói cho chức năng sau:

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

Sau đây là mã tạo:

  __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:

Vì vậy, bạn có bao nhiêu biến cục bộ (thậm chí bên trong if hoặc switch), chỉ 3880 sẽ thay đổi thành giá trị khác. Trừ khi bạn không có biến cục bộ, hướng dẫn này chỉ cần thực thi. Vì vậy, phân bổ biến cục bộ không có chi phí.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.