Làm thế nào hiệu quả là malloc và làm thế nào để thực hiện khác nhau?


8

Nếu tôi sử dụng malloc, mallocluôn luôn sử dụng cùng một thuật toán bất kể nó được phân bổ hay nó nhìn vào dữ liệu và chọn một thuật toán phù hợp?

Chúng ta có thể làm cho malloc nhanh hơn hoặc thông minh hơn bằng cách chọn một thuật toán hiệu quả hơn không? Trong các thử nghiệm của tôi, hệ thống chính thức dựng sẵn malloccủa Ubuntu chậm hơn 10 lần so với dự án trường học nếu kết quả kiểm tra của tôi là chính xác. Nắm bắt được những gì? Tôi ngạc nhiên khi mallocthực hiện quá tệ trong các bài kiểm tra vì nó phải được tối ưu hóa. Có phải nó luôn luôn sử dụng cùng một thuật toán? Có một tham chiếu thực hiện malloc? Nếu tôi muốn xem nguồn malloc, tôi nên xem cái nào? Các bài kiểm tra tôi chạy như sau:

/* returns an array of arrays of char*, all of which NULL */
char ***alloc_matrix(unsigned rows, unsigned columns) {
    char ***matrix = malloc(rows * sizeof(char **));
    unsigned row = 0;
    unsigned column = 0;
    if (!matrix) abort();

    for (row = 0; row < rows; row++) {
        matrix[row] = calloc(columns, sizeof(char *));
        if (!matrix[row]) abort();
        for (column = 0; column < columns; column++) {
            matrix[row][column] = NULL;
        }
    }
    return matrix;
}

/* deallocates an array of arrays of char*, calling free() on each */
void free_matrix(char ***matrix, unsigned rows, unsigned columns) {
    unsigned row = 0;
    unsigned column = 0;
    for (row = 0; row < rows; row++) {
        for (column = 0; column < columns; column++) {
            /*    printf("column %d row %d\n", column, row);*/
            free(matrix[row][column]);
        }
        free(matrix[row]);
    }
    free(matrix);
}


int main(int agrc, char **argv) {

    int x = 10000;
    char *** matrix = alloc_matrix(x, x);
    free_matrix(matrix, x, x);
    return (0);
}

Bài kiểm tra có ổn không? Tôi cũng sử dụng thử nghiệm này:

   for (i = 0; i < 1000000; i++) {
        void *p = malloc(1024 * 1024 * 1024);
        free(p);
   }
  • cập nhật

Theo nhận xét, tôi nên tạo các khối có kích thước thay đổi và miễn phí theo thứ tự khác với phân bổ, vì vậy tôi thử:

int main(int agrc, char **argv) {
     int i;
    srand(time(NULL));
    int randomnumber;
    int size = 1024;
    void *p[size];
    for (i = 0; i < size; i++) {
        randomnumber = rand() % 10;
        p[i] = malloc(1024 * 1024 * randomnumber);
    }

    for (i = size-1; i >= 0; i--) {
        free(p[i]);
    }

    int x = 1024;
    char *** matrix = alloc_matrix(x, x);
    free_matrix(matrix, x, x);
    return (0);
}

Sau đó, malloc tùy chỉnh của tôi không còn nhanh hơn:

$ time ./gb_quickfit 

real    0m0.154s
user    0m0.008s
sys 0m0.144s
dac@dac-Latitude-E7450:~/ClionProjects/omalloc/openmalloc/overhead$ time ./a.out 

real    0m0.014s
user    0m0.008s
sys 0m0.004s

Thuật toán tôi sử dụng là:

void *malloc_quick(size_t nbytes) {

    Header *moreroce(unsigned);
    int index, i;
    index = qindex(nbytes);

    /* 
     * Use another strategy for too large allocations. We want the allocation
     * to be quick, so use malloc_first().
     */

    if (index >= NRQUICKLISTS) {
        return malloc_first(nbytes);
    }

    /* Initialize the quick fit lists if this is the first run. */
    if (first_run) {
        for (i = 0; i < NRQUICKLISTS; ++i) {
            quick_fit_lists[i] = NULL;
        }
        first_run = false;
    }


    /*
     * If the quick fit list pointer is NULL, then there are no free memory
     * blocks present, so we will have to create some before continuing.
     */

    if (quick_fit_lists[index] == NULL) {
        Header* new_quick_fit_list = init_quick_fit_list(index);
        if (new_quick_fit_list == NULL) {
            return NULL;
        } else {
            quick_fit_lists[index] = new_quick_fit_list;
        }
    }

    /*
     * Now that we know there is at least one free quick fit memory block,
     * let's use return that and also update the quick fit list pointer so that
     * it points to the next in the list.
     */

    void* pointer_to_return = (void *)(quick_fit_lists[index] + 1);
    quick_fit_lists[index] = quick_fit_lists[index]->s.ptr;
   /* printf("Time taken %d seconds %d milliseconds", msec/1000, msec%1000);*/
    return pointer_to_return;
}

7
Hãy thử so sánh hiệu suất khi bạn phân bổ và giải quyết các khối có kích thước thay đổi và khi giải phóng không được gọi theo thứ tự giống như phân bổ, và xem điều gì xảy ra với dự án trường học của bạn sau đó.
whatsisname

1
Bạn có thể sẽ tìm thấy nguồn thư viện C ở đây: gnu.org/software/libc - hoặc bạn có thể sử dụng trình quản lý gói của mình để tải xuống nguồn.
kdgregory

1
Nếu bạn muốn nhận xét về lý do tại sao bộ cấp phát của bạn có thể nhanh hơn hoặc chậm hơn thư viện chuẩn, bạn nên hiển thị nó thay vì chỉ là chương trình thử nghiệm. Tôi đoán rằng bạn phân bổ trước một khối lớn bộ nhớ và sau đó khắc các khối, điều đó có nghĩa là bạn không phải trả giá tăng dần kích thước heap thông qua sbrk(hoặc bất kỳ phân bổ hiện đại nào sử dụng).
kdgregory

1
Và không liên quan, tại sao callocvà sau đó rõ ràng rõ ràng?
kdgregory

@whatsisname Tôi đã thay đổi bài kiểm tra theo nhận xét của bạn và tôi nhận được kết quả hợp lý so với tùy chỉnh của tôi mallocchậm hơn. Đó là những gì tôi mong đợi.
Niklas

Câu trả lời:


11

Có nhiều triển khai mallocvà họ có thể sử dụng các thuật toán rất khác nhau. Hai triển khai được sử dụng rất rộng rãi là jemallocdlmalloc . Trong cả hai trường hợp, các liên kết có rất nhiều thông tin về quá trình suy nghĩ và sự đánh đổi được thực hiện trong một công cụ phân bổ mục đích chung.

Hãy nhớ rằng mallocviệc triển khai có rất ít thông tin để tiếp tục, chỉ là quy mô phân bổ được yêu cầu. Một freetriển khai chỉ có con trỏ và bất kỳ dữ liệu nào 'malloc' có thể được bí mật gắn vào nó. Như vậy, kết cục là một số lượng lớn các heuristic tức là phỏng đoán được truyền cảm hứng trong việc quyết định cách phân bổ và phân bổ.

Một số vấn đề mà người cấp phát cần giải quyết là:

  1. căn chỉnh - một số căn chỉnh bộ nhớ nhanh hơn những cái khác
  2. phân mảnh - phân bổ ngây thơ và giải phóng có thể để lại lỗ hổng gây phình to
  3. hiệu suất - quay trở lại hệ điều hành để có thêm bộ nhớ có thể tốn kém
  4. Các yêu cầu phổ biến - trong các ứng dụng thực tế (đặc biệt là C ++) thường thực hiện rất nhiều phân bổ nhỏ, do đó, việc thực hiện các hiệu quả này có thể giúp ích rất nhiều

Nếu tính tất cả những điều này, những gì bạn sẽ thấy là các bộ cấp phát có xu hướng là những phần mềm phức tạp để trong sử dụng chung, chúng hoạt động tốt. Tuy nhiên, trong các trường hợp cụ thể, có thể tốt hơn để quản lý bộ nhớ bên ngoài bộ cấp phát nếu bạn tình cờ biết nhiều hơn về cấu trúc của dữ liệu. Rõ ràng nhược điểm là bạn cần tự mình thực hiện công việc.


+1 cho liên kết đến các bài viết tốt. Tôi cần nghiên cứu lý thuyết. Chỉ vấp ngã vallocmà tôi chưa bao giờ nghe nói trước đây, cần phải kiểm tra xem nó là gì.
Niklas

2
Đừng quên an toàn chủ đề.
Sebastian Redl

valloctrả về bộ nhớ được căn chỉnh theo kích thước trang. Nó không được dùng vì bạn có thể sử dụng memaligncho điều đó.
Alex

19

Nếu bạn chỉ quan tâm đến hiệu quả, đây là một tiêu chuẩn phù hợp và triển khai rất hiệu quả :

void* malloc(size_t sz) {
  errno = ENOMEM;
  return NULL;
}

void free(void*p) {
  if (p != NULL) abort();
}

Tôi khá chắc chắn rằng bạn sẽ không tìm thấy bất kỳ nhanh hơn malloc.

Nhưng trong khi vẫn tuân thủ tiêu chuẩn, việc thực hiện đó là vô ích (nó không bao giờ phân bổ thành công bất kỳ bộ nhớ heap nào). Đó là một trò đùa thực hiện.

Điều này minh họa rằng trong thế giới thực, malloc& freeviệc triển khai cần phải đánh đổi . Và các triển khai khác nhau đang làm cho sự đánh đổi khác nhau. Bạn sẽ tìm thấy nhiều hướng dẫn về việc triển khai malloc . Để gỡ lỗi rò rỉ bộ nhớ trong các chương trình C của bạn, bạn sẽ muốn sử dụng valgrind .

Lưu ý rằng trên các hệ thống Linux ít nhất, (hầu hết) tất cả các thư viện chuẩn C đều là phần mềm miễn phí , vì vậy bạn có thể nghiên cứu mã nguồn của chúng (đặc biệt là mã cho malloc&free ). musl-libc có một số mã nguồn rất dễ đọc.

BTW, mã trong câu hỏi của bạn là sai (và sẽ sụp đổ với câu hỏi malloctrên của tôi ). Mỗi cuộc gọi đếnmalloc đều có thể thất bại, và bạn nên kiểm tra điều đó.

Bạn có thể muốn đọc Lập trình Linux nâng cao và một số tài liệu tổng quát hơn về các hệ điều hành, ví dụ: Hệ điều hành: ba phần dễ dàng .

Bạn có lẽ nên đọc một cái gì đó về bộ sưu tập rác , ít nhất là để có được các khái niệm & thuật ngữ chính; có lẽ bằng cách đọc cẩm nang GC . Lưu ý rằng đếm tham chiếu có thể được xem như một dạng của GC (một dạng kém, không xử lý tốt các chu kỳ tham chiếu hoặc đồ thị theo chu kỳ ...).

Bạn có thể muốn sử dụng trong chương trình thu gom rác bảo thủ của chương trình C của Boehm : sau đó bạn sẽ sử dụng GC_MALLOC(hoặc, cho dữ liệu không có con trỏ như chuỗi hoặc mảng số, GC_MALLOC_ATOMIC) thay vìmalloc và bạn sẽ không bận tâm về việc gọi freenữa. Có một số cảnh báo khi sử dụng GC của Boehm. Bạn có thể xem xét các chương trình hoặc thư viện GC khác ...

Lưu ý: Để kiểm tra lỗi malloc trên hệ thống Linux ( mallocđôi khi sẽ gọi hệ thống mmap (2) và / hoặc sbrk (2) trên Linux để tăng không gian địa chỉ ảo , nhưng thường thì nó cố gắng sử dụng lại freebộ nhớ d trước đó ) , bạn có thể gọi setrlimit (2) một cách thích hợp với RLIMIT_ASvà / hoặc RLIMIT_DATA, thường là thông qua phần ulimitdựng sẵn của vỏ thiết bị đầu cuối của bạn. Sử dụng strace (1) để tìm hiểu các cuộc gọi hệ thống được thực hiện bởi chương trình của bạn (hoặc một số khác).


Tôi quan tâm đến độ tin cậy nhưng dễ hiểu hơn về hiệu quả / tốc độ. Tôi đọc rằng malloc có thể bị sập nếu nó bị gián đoạn hoặc tương tự, nhưng tôi chưa biết đủ về điều đó. Cảm ơn đã chỉ ra rằng mã kiểm tra là sai, tôi nghĩ rằng kết quả là không hợp lý. Tôi đã thay đổi mã để phân bổ ngẫu nhiên. Tôi nghĩ rằng kết luận là tôi nên nghiên cứu thêm.
Niklas

Có những triển khai trong đó malloc không bao giờ thất bại (tuy nhiên chương trình của bạn có thể bị sập). Trong thử nghiệm iOS, liệu malloc trả về con trỏ S null là khá vô nghĩa.
gnasher729

Tôi biết rằng (ví dụ: một số máy tính Linux có bộ nhớ quá mức), nhưng tôi nhận thấy rằng việc triển khai như vậy là trái với tiêu chuẩn C: chúng mallocđang trả về một NULLcon trỏ (không ) không thể bị loại bỏ.
Basile Starynkevitch

6

Đầu tiên, malloc và công việc tự do cùng nhau, vì vậy việc thử nghiệm malloc tự nó là sai lệch. Thứ hai, cho dù chúng tốt đến đâu, chúng có thể dễ dàng là chi phí vượt trội trong bất kỳ ứng dụng nào, và giải pháp tốt nhất cho điều đó là gọi chúng ít hơn. Gọi điện cho họ ít là gần như luôn luôn là cách chiến thắng cho các chương trình sửa chữa mà là malloc -limited. Một cách phổ biến để làm điều này là tái chế các đối tượng đã sử dụng. Khi bạn hoàn thành một khối, thay vì giải phóng nó, bạn sẽ xâu chuỗi nó vào ngăn xếp khối đã sử dụng và sử dụng lại vào lần tiếp theo bạn cần.


5

Vấn đề chính với malloc_quick()việc cấy ghép của bạn là, nó không an toàn cho chuỗi. Và có, nếu bạn bỏ qua hỗ trợ luồng từ bộ cấp phát của mình, bạn có thể đạt được mức tăng hiệu suất đáng kể. Tôi đã thấy một sự tăng tốc tương tự với bộ cấp phát không an toàn của riêng tôi.

Tuy nhiên, việc thực hiện tiêu chuẩn cần phải an toàn cho luồng. Nó cần phải tính đến tất cả những điều sau đây:

  • Chủ đề khác nhau sử dụng malloc()free()đồng thời. Điều đó có nghĩa là, việc triển khai không thể truy cập trạng thái toàn cầu mà không cần đồng bộ hóa nội bộ.

    Vì các khóa thực sự tốn kém, nên malloc()việc triển khai điển hình cố gắng tránh sử dụng trạng thái toàn cầu càng nhiều càng tốt bằng cách sử dụng lưu trữ luồng cục bộ cho hầu hết tất cả các yêu cầu.

  • Một luồng phân bổ một con trỏ không nhất thiết là luồng giải phóng nó. Điều này phải được quan tâm.

  • Một luồng có thể liên tục phân bổ bộ nhớ và chuyển nó sang một luồng khác để giải phóng nó. Điều này làm cho việc xử lý điểm cuối khó khăn hơn nhiều, vì điều đó có nghĩa là các khối miễn phí có thể tích lũy trong bất kỳ lưu trữ cục bộ luồng nào. Điều này buộc việc malloc()triển khai phải cung cấp phương tiện cho các luồng để trao đổi các khối miễn phí và đôi khi có thể yêu cầu phải lấy khóa.

Nếu bạn không quan tâm đến các luồng, tất cả các điểm này hoàn toàn không có vấn đề gì, do đó, một công cụ phân bổ không an toàn cho luồng không phải trả tiền cho việc xử lý chúng với hiệu suất. Nhưng, như tôi đã nói, việc thực hiện tiêu chuẩn không thể bỏ qua những vấn đề này và do đó phải trả tiền cho việc xử lý của họ.


2

Tôi nghĩ rằng hai SUT không phải là so sánh trực tiếp. Tôi sẽ không ngạc nhiên về bất kỳ sự khác biệt nào có thể so sánh khi bạn xem xét tất cả các biến: sản xuất bộ nhớ, kiến ​​trúc bo mạch chủ, phiên bản trình biên dịch (đã biên dịch malloc), ứng dụng không gian bộ nhớ giống như thế nào trên SUT vào thời điểm đó, v.v. .... Hãy thử sử dụng khai thác thử nghiệm của bạn để thể hiện rõ hơn cách bạn sẽ sử dụng bộ nhớ trong một dự án thực tế - với một số phân bổ lớn / nhỏ và một số ứng dụng được giữ trong một thời gian dài và một số được giải phóng ngay sau khi được sử dụng.


2

Nếu bạn so sánh việc triển khai malloc thực với dự án trường học, hãy xem xét rằng malloc thực phải quản lý phân bổ, phân bổ và giải phóng bộ nhớ với các kích cỡ khác nhau, hoạt động chính xác nếu phân bổ xảy ra đồng thời trên các luồng khác nhau, và phân bổ lại và giải phóng bộ nhớ xảy ra trên các luồng khác nhau . Bạn cũng muốn chắc chắn rằng mọi nỗ lực giải phóng bộ nhớ không được phân bổ với malloc đều bị bắt. Và cuối cùng, hãy chắc chắn rằng bạn không so sánh với việc triển khai malloc gỡ lỗ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.