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
}
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
}
Câu trả lời:
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];
và len
vượ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ó.
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.
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 đó len
là quá lớn. Nếu bạn biết chắc chắn rằng nó len
sẽ 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 malloc
và free
.
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
.
str
phân rã thành một con trỏ , vì vậy nó sizeof
sẽ 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.
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ự)
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.
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.
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.
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.
Đâ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 realloc
và calloc
và 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 FastMem
thể 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ỏ.
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ụ calloc
hoặ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ụ calloc
hoặc malloc
quay 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 malloc
có 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 free
chỉ 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 calloc
và free
trong 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 buf
thay vì result
bê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 malloc
cho 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.