Mảng hay Malloc?


12

Tôi đang sử dụng mã sau đây trong ứng dụng của mình và nó hoạt động tốt. Nhưng tôi đang tự hỏi liệu nó tốt hơn để làm nó với malloc hay để nó như vậy?

function (int len)
{
char result [len] = some chars;
send result over network
}

2
Là giả định rằng mã được nhắm mục tiêu cho một môi trường không nhúng?
tehnyit

Câu trả lời:


27

Sự khác biệt chính là VLAs (mảng có chiều dài thay đổi) không cung cấp cơ chế phát hiện lỗi phân bổ.

Nếu bạn tuyên bố

char result[len];

lenvượt quá dung lượng ngăn xếp có sẵn, hành vi của chương trình của bạn không được xác định. Không có cơ chế ngôn ngữ nào để xác định trước liệu phân bổ sẽ thành công hay để xác định sau thực tế liệu nó có thành công hay không.

Mặt khác, nếu bạn viết:

char *result = malloc(len);
if (result == NULL) {
    /* allocation failed, abort or take corrective action */
}

sau đó bạn có thể xử lý các thất bại một cách duyên dáng hoặc ít nhất đảm bảo rằng chương trình của bạn sẽ không cố gắng tiếp tục thực hiện sau một thất bại.

(. Vâng, chủ yếu là hệ thống Trên Linux, malloc()có thể phân bổ một đoạn không gian địa chỉ thậm chí nếu không có lưu trữ tương ứng có sẵn; nỗ lực hơn để sử dụng không gian có thể gọi oom killer Nhưng kiểm tra. malloc()Thất bại vẫn là thực hành tốt.)

Một vấn đề khác, trên nhiều hệ thống, là có nhiều không gian hơn (có thể là nhiều không gian hơn) dành malloc()cho các đối tượng tự động như VLAs.

Và như câu trả lời của Philip đã được đề cập, VLAs đã được thêm vào C99 (đặc biệt là Microsoft không hỗ trợ họ).

Và VLAs đã được thực hiện tùy chọn trong C11. Có lẽ hầu hết các trình biên dịch C11 sẽ hỗ trợ chúng, nhưng bạn không thể tin vào nó.


14

Mảng tự động có độ dài thay đổi được giới thiệu cho C trong C99.

Trừ khi bạn lo ngại về khả năng so sánh ngược với các tiêu chuẩn cũ, không sao cả.

Nói chung, nếu nó hoạt động, đừng chạm vào nó. Đừng tối ưu hóa trước thời hạn. Đừng lo lắng về việc thêm các tính năng đặc biệt hoặc cách làm thông minh, bởi vì bạn thường sẽ không sử dụng nó. Giữ cho nó đơn giản.


7
Tôi phải không đồng ý với câu lệnh "nếu nó hoạt động, đừng chạm vào nó". Sai lầm khi tin rằng một số mã "hoạt động" có thể khiến bạn phải giải quyết các vấn đề trong một số mã "hoạt động". Niềm tin phải được thay thế bằng sự chấp nhận dự kiến ​​rằng một số mã hoạt động ngay bây giờ.
Bruce Ediger

2
Đừng chạm vào nó cho đến khi bạn tạo một phiên bản có thể "hoạt động" tốt hơn ...
H_7

8

Nếu trình biên dịch của bạn hỗ trợ các mảng có độ dài thay đổi, mối nguy hiểm duy nhất là tràn ngăn xếp trên một số hệ thống, khi đó lenlà quá lớn. Nếu bạn biết chắc chắn rằng nó lensẽ không lớn hơn một số nhất định và bạn biết rằng ngăn xếp của bạn sẽ không bị tràn ngay cả ở độ dài tối đa, hãy để lại mã như hiện tại; mặt khác, viết lại nó với mallocfree.


những gì về điều này trên hàm không c99 (char []) {char result [sizeof (char)] = một số ký tự; gửi kết quả qua mạng}
Dev Bag

@DevBag char result [sizeof(char)]là một mảng có kích thước 1(vì sizeof(char)bằng một), vì vậy bài tập sẽ bị cắt bớt some chars.
dasblinkenlight

xin lỗi về điều đó, ý tôi là nó theo cách này (char str []) {char result [sizeof (str)] = some chars; gửi kết quả qua mạng}
Dev Bag

4
@DevBag Điều này cũng sẽ không hoạt động - str phân rã thành một con trỏ , vì vậy nó sizeofsẽ là bốn hoặc tám, tùy thuộc vào kích thước con trỏ trên hệ thống của bạn.
dasblinkenlight

2
Nếu bạn đang sử dụng phiên bản C không có mảng có chiều dài thay đổi, bạn có thể thực hiện việc char* result = alloca(len);phân bổ trên ngăn xếp. Nó có cùng hiệu ứng cơ bản (và các vấn đề cơ bản tương tự)
Gort the Robot

6

Tôi thích ý tưởng rằng bạn có thể có một mảng được phân bổ thời gian chạy mà không bị phân mảnh bộ nhớ, con trỏ lơ lửng, v.v. Tuy nhiên, những người khác đã chỉ ra rằng việc phân bổ thời gian chạy này có thể âm thầm thất bại. Vì vậy, tôi đã thử điều này bằng cách sử dụng gcc 4.5.3 trong môi trường bash Cygwin:

#include <stdio.h>
#include <string.h>

void testit (unsigned long len)
{
    char result [len*2];
    char marker[100];

    memset(marker, 0, sizeof(marker));
    printf("result's size: %lu\n", sizeof(result));
    strcpy(result, "this is a test that should overflow if no allocation");
    printf("marker's contents: '%s'\n", marker);
}

int main(int argc, char *argv[])
{
    testit(100);
    testit((unsigned long)-1);  // probably too big
}

Đầu ra là:

$ ./a.exe
result's size: 200
marker's contents: ''
result's size: 4294967294
marker's contents: 'should overflow if no allocation'

Độ dài quá lớn được chuyển trong cuộc gọi thứ hai rõ ràng gây ra lỗi (tràn vào điểm đánh dấu []). Điều này không có nghĩa là loại kiểm tra này là bằng chứng ngu ngốc (những kẻ ngốc có thể thông minh!) Hoặc nó đáp ứng các tiêu chuẩn của C99, nhưng nó có thể giúp ích nếu bạn có mối quan tâm đó.

Như thường lệ, YMMV.


1
+1 điều này rất hữu ích: 3
Kokizzu

Thật tuyệt khi có một số mã đi cùng với yêu cầu của mọi người! Cảm ơn ^ _ ^
Musa Al-hassy

3

Nói chung, ngăn xếp là nơi dễ nhất và tốt nhất để đặt dữ liệu của bạn.

Tôi sẽ tránh các vấn đề của VLAs bằng cách phân bổ mảng lớn nhất mà bạn mong đợi.

Tuy nhiên, có những trường hợp khi heap là tốt nhất và lộn xộn với malloc là giá trị nỗ lực.

  1. Khi lượng dữ liệu lớn nhưng thay đổi. Lớn phụ thuộc vào môi trường của bạn> 1K cho các hệ thống nhúng,> 10MB cho máy chủ Doanh nghiệp.
  2. Khi bạn muốn dữ liệu tồn tại sau khi bạn thoát khỏi thói quen của mình, ví dụ: nếu bạn trả lại một con trỏ cho dữ liệu của mình. Sử dụng
  3. Một sự kết hợp giữa con trỏ tĩnh và malloc () thường tốt hơn so với việc xác định một mảng tĩnh lớn;

3

Trong lập trình nhúng, chúng tôi luôn sử dụng mảng tĩnh thay vì malloc khi malloc và các hoạt động miễn phí thường xuyên. Do thiếu quản lý bộ nhớ trong hệ thống nhúng, việc phân bổ thường xuyên và hoạt động miễn phí sẽ gây ra sự phân mảnh bộ nhớ. Nhưng chúng ta nên sử dụng một số phương pháp phức tạp như xác định kích thước tối đa của mảng và sử dụng mảng cục bộ tĩnh.

Nếu ứng dụng của bạn đang chạy trong Linux hoặc Windows, không có vấn đề gì khi sử dụng mảng hoặc malloc. Điểm mấu chốt nằm ở nơi bạn sử dụng cấu trúc ngày và logic mã của mình.


1

Một điều mà chưa ai đề cập đến là tùy chọn mảng có độ dài thay đổi có thể sẽ nhanh hơn rất nhiều so với malloc / free vì việc phân bổ VLA chỉ là một trường hợp điều chỉnh con trỏ ngăn xếp (ít nhất là trong GCC).

Vì vậy, nếu chức năng này là một chức năng được gọi là thường xuyên (tất nhiên, bạn sẽ xác định bằng cách định hình), thì VLA là một tùy chọn tối ưu hóa tốt.


1
Nó sẽ có vẻ tốt cho đến khi nó đẩy bạn vào tình huống ngoài không gian. Hơn nữa, nó có thể không phải là mã của bạn thực sự đạt đến giới hạn ngăn xếp; nó có thể sẽ cắn trong thư viện hoặc cuộc gọi hệ thống (hoặc ngắt).
Donal Fellows

@Donal Performance luôn là sự đánh đổi bộ nhớ so với tốc độ. Nếu bạn định đi vòng phân bổ các mảng vài megabyte, thì bạn có một điểm, tuy nhiên, ngay cả đối với một vài kilobyte, miễn là chức năng không được đệ quy, đó là một tối ưu hóa tốt.
JeremyP

1

Đây là một giải pháp C rất phổ biến mà tôi sử dụng cho vấn đề có thể giúp ích. Không giống như VLAs, nó không phải đối mặt với bất kỳ rủi ro thực tế nào về việc tràn ngăn xếp trong các trường hợp bệnh lý.

/// Used for frequent allocations where the common case generally allocates
/// a small amount of memory, at which point a heap allocation can be
/// avoided, but rare cases also need to be handled which may allocate a
/// substantial amount. Note that this structure is not safe to copy as
/// it could potentially invalidate the 'data' pointer. Its primary use
/// is just to allow the stack to be used in common cases.
struct FastMem
{
    /// Stores raw bytes for fast access.
    char fast_mem[512];

    /// Points to 'fast_mem' if the data fits. Otherwise, it will point to a
    /// dynamically allocated memory address.
    void* data;
};

/// @return A pointer to a newly allocated memory block of the specified size.
/// If the memory fits in the specified fast memory structure, it will use that
/// instead of the heap.
void* fm_malloc(struct FastMem* mem, int size)
{
    // Utilize the stack if the memory fits, otherwise malloc.
    mem->data = (size < sizeof mem->fast_mem) ? mem->fast_mem: malloc(size);
    return mem->data;
}

/// Frees the specified memory block if it has been allocated on the heap.
void fm_free(struct FastMem* mem)
{
    // Free the memory if it was allocated dynamically with 'malloc'.
    if (mem->data != mem->fast_mem)
        free(mem->data);
    mem->data = 0;
}

Để sử dụng nó trong trường hợp của bạn:

struct FastMem fm;

// `result` will be allocated on the stack if 'len <= 512'.
char* result = fm_malloc(&fm, len);

// send result over network.
...

// this function will only do a heap deallocation if 'len > 512'.
fm_free(&fm, result);

Điều này làm trong trường hợp trên là sử dụng ngăn xếp nếu chuỗi phù hợp với 512 byte trở xuống. Nếu không, nó sử dụng phân bổ heap. Điều này có thể hữu ích nếu, giả sử, 99% thời gian, chuỗi phù hợp với 512 byte hoặc ít hơn. Tuy nhiên, giả sử có một số trường hợp kỳ lạ điên rồ mà đôi khi bạn có thể cần xử lý trong đó chuỗi là 32 kilobyte nơi người dùng ngủ trên bàn phím của mình hoặc đại loại như thế. Điều này cho phép cả hai tình huống được xử lý mà không có vấn đề.

Các phiên bản thực tế tôi sử dụng trong sản xuất cũng có phiên bản riêng của mình realloccallocvà vân vân cũng như C cấu trúc dữ liệu tiêu chuẩn phù hợp ++ xây dựng trên khái niệm tương tự, nhưng tôi trích tối thiểu cần thiết để minh họa cho khái niệm này.

Nó có một lời cảnh báo rằng việc sao chép xung quanh nó rất nguy hiểm và bạn không nên trả lại các con trỏ được phân bổ thông qua nó (cuối cùng chúng có thể bị vô hiệu hóa khi FastMemthể hiện bị phá hủy). Nó có nghĩa là được sử dụng cho các trường hợp đơn giản trong phạm vi của hàm cục bộ, nơi bạn sẽ luôn luôn muốn sử dụng ngăn xếp / VLAs nếu không một số trường hợp hiếm hoi có thể gây ra tràn bộ đệm / ngăn xếp. Đây không phải là công cụ phân bổ cho mục đích chung và không nên được sử dụng như vậy.

Tôi thực sự đã tạo ra nó từ lâu để đối phó với một tình huống trong một cơ sở mã di sản sử dụng C89 mà một nhóm trước đây nghĩ rằng sẽ không bao giờ xảy ra khi người dùng quản lý để đặt tên cho một mục có tên dài hơn 2047 ký tự (có thể anh ta ngủ trên bàn phím ). Các đồng nghiệp của tôi thực sự đã cố gắng tăng kích thước của các mảng được phân bổ ở nhiều nơi lên 16.384 để đáp ứng tại thời điểm đó tôi nghĩ rằng nó đang trở nên lố bịch và chỉ trao đổi rủi ro tràn chồng nhiều hơn để đổi lấy rủi ro tràn bộ đệm ít hơn. Điều này cung cấp một giải pháp rất dễ dàng để cắm vào để khắc phục những trường hợp đó bằng cách chỉ cần thêm một vài dòng mã. Điều này cho phép trường hợp phổ biến được xử lý rất hiệu quả và vẫn sử dụng ngăn xếp mà không có những trường hợp hiếm hoi điên rồ đó đòi hỏi đống phần mềm bị sập. Tuy nhiên, tôi' đã tìm thấy nó hữu ích kể từ đó ngay cả sau C99 vì VLAs vẫn không thể bảo vệ chúng tôi chống lại tràn ngăn xếp. Điều này có thể nhưng vẫn nhóm từ ngăn xếp cho các yêu cầu phân bổ nhỏ.


1

Các cuộc gọi stack luôn có giới hạn. Trên các hệ điều hành chính như Linux hoặc Windows, giới hạn là một hoặc một vài megabyte (và bạn có thể tìm cách thay đổi nó). Với một số ứng dụng đa luồng, nó có thể thấp hơn (vì các luồng có thể được tạo với ngăn xếp nhỏ hơn). Trên các hệ thống nhúng, nó có thể nhỏ đến vài kilobyte. Một nguyên tắc nhỏ là tránh các khung gọi lớn hơn vài kilobyte.

Vì vậy, sử dụng VLA chỉ có ý nghĩa nếu bạn chắc chắn rằng mình lenđủ nhỏ (nhiều nhất là vài chục nghìn). Nếu không, bạn có một ngăn xếp tràn và đó là một trường hợp hành vi không xác định , một tình huống rất đáng sợ .

Tuy nhiên, sử dụng cấp phát bộ nhớ động thủ công C (ví dụ callochoặc malloc&free ) cũng có nhược điểm:

  • nó có thể thất bại và bạn phải luôn luôn kiểm tra thất bại (ví dụ callochoặc mallocquay lại NULL).

  • nó chậm hơn: phân bổ VLA thành công mất vài nano giây, thành công malloccó thể cần vài micro giây (trong trường hợp tốt, chỉ một phần của micro giây) hoặc thậm chí nhiều hơn (trong các trường hợp bệnh lý liên quan đến đập , nhiều hơn nữa).

  • mã hóa khó hơn nhiều: bạn freechỉ có thể khi bạn chắc chắn rằng vùng nhọn không còn được sử dụng nữa. Trong trường hợp của bạn, bạn có thể gọi cả hai callocfreetrong cùng một thói quen.

Nếu bạn biết rằng hầu hết thời gian của bạn result (một tên rất kém, bạn không bao giờ nên trả lại địa chỉ của một biến VLA tự động ; vì vậy tôi đang sử dụng bufthay vì resultbên dưới) là nhỏ, bạn có thể đặc biệt, ví dụ:

char tinybuf[256];
char *buf = (len<sizeof(tinybuf))?tinybuf:malloc(len);
if (!buf) { perror("malloc"); exit(EXIT_FAILURE); };
fill_buffer(buf, len);
send_buffer_on_network(buf, len);
if (buf != tinybuf) 
  free(buf);

Tuy nhiên, đoạn mã trên ít đọc hơn và có thể là tối ưu hóa sớm. Tuy nhiên, nó mạnh hơn một giải pháp VLA thuần túy.

Tái bút Một số hệ thống (ví dụ: một số bản phân phối Linux được bật theo mặc định) có bộ nhớ thừa (điều này làm malloccho việc đưa ra một số con trỏ ngay cả khi không có đủ bộ nhớ). Đây là một tính năng tôi không thích và thường tắt trên các máy Linux của tôi.

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.