Một ví dụ về chiều dài biến C tốt [đã đóng]


9

Câu hỏi này nhận được một sự tiếp nhận đóng băng tại SO, vì vậy tôi quyết định xóa nó ở đó và thử ở đây để thay thế. Nếu bạn nghĩ rằng nó cũng không phù hợp ở đây, ít nhất vui lòng để lại nhận xét về đề xuất cách tìm ví dụ tôi sau ...

Bạn có thể đưa ra một ví dụ , trong đó việc sử dụng C99 VLA mang lại lợi thế thực sự so với những thứ như cơ chế sử dụng C ++ RAII tiêu chuẩn hiện tại không?

Ví dụ tôi sau nên:

  1. Đạt được lợi thế hiệu suất dễ dàng đo được (10% có thể) so với sử dụng heap.
  2. Không có một cách giải quyết tốt, sẽ không cần toàn bộ mảng.
  3. Thực tế được hưởng lợi từ việc sử dụng kích thước động, thay vì kích thước tối đa cố định.
  4. Không thể gây ra tràn ngăn xếp trong kịch bản sử dụng bình thường.
  5. Đủ mạnh để cám dỗ nhà phát triển cần hiệu năng để đưa tệp nguồn C99 vào dự án C ++.

Thêm một số làm rõ về ngữ cảnh: Ý tôi là VLA có nghĩa là C99 và không được bao gồm trong C ++ tiêu chuẩn: int array[n]đâu nlà một biến. Và tôi sau một ví dụ về trường hợp sử dụng trong đó nó bỏ qua các lựa chọn thay thế được cung cấp bởi các tiêu chuẩn khác (C90, C ++ 11):

int array[MAXSIZE]; // C stack array with compile time constant size
int *array = calloc(n, sizeof int); // C heap array with manual free
int *array = new int[n]; // C++ heap array with manual delete
std::unique_ptr<int[]> array(new int[n]); // C++ heap array with RAII
std::vector<int> array(n); // STL container with preallocated size

Một vài ý tưởng:

  • Các hàm lấy varargs, tự nhiên giới hạn số lượng vật phẩm ở mức hợp lý, nhưng không có bất kỳ giới hạn trên mức API hữu ích nào.
  • Hàm đệ quy, nơi ngăn xếp lãng phí là không mong muốn
  • Nhiều phân bổ nhỏ và phát hành, trong đó chi phí heap sẽ là xấu.
  • Xử lý các mảng đa chiều (như ma trận có kích thước tùy ý), trong đó hiệu suất là rất quan trọng và các chức năng nhỏ dự kiến ​​sẽ được đưa vào rất nhiều.
  • Từ nhận xét: thuật toán đồng thời, trong đó phân bổ heap có phí đồng bộ hóa .

Wikipedia có một ví dụ không đáp ứng các tiêu chí của tôi , bởi vì sự khác biệt thực tế đối với việc sử dụng heap dường như không liên quan ít nhất là không có ngữ cảnh. Nó cũng không lý tưởng, vì nếu không có nhiều ngữ cảnh hơn, có vẻ như số lượng vật phẩm rất có thể gây ra tràn ngăn xếp.

Lưu ý: Tôi đặc biệt theo mã ví dụ hoặc đề xuất thuật toán sẽ có lợi cho việc này, để tôi tự thực hiện ví dụ.


1
Một chút suy đoán (vì đây là một cái búa tìm kiếm một cái đinh), nhưng có lẽ alloca()sẽ thực sự tồn malloc()tại lâu hơn trong một môi trường đa luồng vì sự tranh chấp khóa sau này . Nhưng đây là một sự kéo dài thực sự vì các mảng nhỏ chỉ nên sử dụng một kích thước cố định và các mảng lớn có thể sẽ cần cả đống.
chrisaycock

1
@chrisaycock Vâng, rất nhiều búa tìm kiếm một cái đinh, nhưng một cái búa thực sự tồn tại (có thể là C99 VLA hoặc không thực sự trong bất kỳ tiêu chuẩn nào alloca, mà tôi nghĩ về cơ bản là giống nhau). Nhưng điều đa luồng đó là tốt, chỉnh sửa câu hỏi để bao gồm nó!
hyde

Một nhược điểm của VLAs là không có cơ chế phát hiện lỗi phân bổ; nếu không đủ bộ nhớ, hành vi không được xác định. (Điều này cũng đúng với các mảng có kích thước cố định - và cho alloca ().)
Keith Thompson

@KeithThndry Vâng, không có gì đảm bảo rằng malloc / new phát hiện lỗi phân bổ, ví dụ, xem trang Ghi chú cho Linux malloc man ( linux.die.net/man/3/malloc ).
hyde

@hyde: Và điều gây tranh cãi là liệu mallochành vi của Linux có phù hợp với tiêu chuẩn C hay không.
Keith Thompson

Câu trả lời:


9

Tôi vừa hack một chương trình nhỏ tạo ra một tập hợp các số ngẫu nhiên khởi động lại ở cùng một hạt giống mỗi lần, để đảm bảo rằng nó "công bằng" và "có thể so sánh". Khi nó đi cùng, nó chỉ ra tối thiểu và tối đa của các giá trị này. Và khi nó đã tạo ra tập hợp các số, nó sẽ đếm có bao nhiêu trên trung bình minmax.

Đối với RẤT nhỏ mảng, nó cho thấy lợi ích rõ ràng với VLA hơn std::vector<>.

Đây không phải là vấn đề thực sự, nhưng chúng ta có thể dễ dàng tưởng tượng thứ gì đó mà chúng ta sẽ đọc các giá trị từ một tệp nhỏ thay vì sử dụng các số ngẫu nhiên và thực hiện một số phép tính đếm / phút / tối đa khác có ý nghĩa hơn với cùng một loại mã .

Đối với RẤT các giá trị nhỏ của "số lượng số ngẫu nhiên" (x) trong các hàm có liên quan, vlagiải pháp sẽ thắng với một mức chênh lệch lớn. Khi kích thước lớn hơn, "phần thắng" sẽ nhỏ hơn và được cung cấp đủ kích thước, giải pháp vectơ có vẻ hiệu quả hơn - không nghiên cứu biến thể đó quá nhiều, vì khi chúng ta bắt đầu có hàng ngàn phần tử trong một VLA, thì không thực sự những gì họ dự định làm ...

Và tôi chắc chắn rằng ai đó sẽ nói với tôi rằng có một số cách viết tất cả mã này với một loạt các mẫu và làm cho nó thực hiện điều này mà không cần chạy nhiều hơn RDTSC và coutcác bit khi chạy ... Nhưng tôi không nghĩ đó thực sự là điểm

Khi chạy biến thể đặc biệt này, tôi nhận được khoảng 10% chênh lệch giữa func1(VLA) và func2(std :: vector).

count = 9884
func1 time in clocks per iteration 7048685
count = 9884
func2 time in clocks per iteration 7661067
count = 9884
func3 time in clocks per iteration 8971878

Điều này được biên soạn với: g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

Đây là mã:

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstdlib>

using namespace std;

const int SIZE = 1000000;

uint64_t g_val[SIZE];


static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


int func1(int x)
{
    int v[x];

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}

int func2(int x)
{
    vector<int> v;
    v.resize(x); 

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

int func3(int x)
{
    vector<int> v;

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v.push_back(rand() % x);
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

void runbench(int (*f)(int), const char *name)
{
    srand(41711211);
    uint64_t long t = rdtsc();
    int count = 0;
    for(int i = 20; i < 200; i++)
    {
        count += f(i);
    }
    t = rdtsc() - t;
    cout << "count = " << count << endl;
    cout << name << " time in clocks per iteration " << dec << t << endl;
}

struct function
{
    int (*func)(int);
    const char *name;
};


#define FUNC(f) { f, #f }

function funcs[] = 
{
    FUNC(func1),
    FUNC(func2),
    FUNC(func3),
}; 


int main()
{
    for(size_t i = 0; i < sizeof(funcs)/sizeof(funcs[0]); i++)
    {
        runbench(funcs[i].func, funcs[i].name);
    }
}

Ồ, hệ thống của tôi cho thấy sự cải thiện 30% trong phiên bản VLA std::vector.
chrisaycock

1
Chà, hãy thử với phạm vi kích thước khoảng 5-15 thay vì 20-200 và có thể bạn sẽ có sự cải thiện 1000% trở lên. [Cũng phụ thuộc vào tùy chọn trình biên dịch - Tôi sẽ chỉnh sửa đoạn mã trên để hiển thị các tùy chọn trình biên dịch của mình trên gcc]
Mats Petersson

Tôi chỉ thêm một func3trong đó sử dụng v.push_back(rand())thay vì v[i] = rand();và loại bỏ sự cần thiết cho resize(). Phải mất khoảng 10% lâu hơn so với sử dụng resize(). [Tất nhiên, trong quá trình này, tôi thấy rằng việc sử dụng v[i]là một đóng góp chính cho thời gian mà chức năng thực hiện - tôi hơi ngạc nhiên về điều đó].
Thảm Petersson

1
@MikeBrown Bạn có biết một std::vectortriển khai thực tế sẽ sử dụng VLA / alloca, hay đó chỉ là suy đoán?
hyde

3
Vectơ thực sự sử dụng một mảng bên trong, nhưng theo tôi hiểu, nó không có cách nào để sử dụng một VLA. Tôi tin rằng ví dụ của tôi cho thấy rằng VLA rất hữu ích trong một số trường hợp (thậm chí là nhiều) trong đó lượng dữ liệu nhỏ. Ngay cả khi vectơ phát hiện ra VLA, nó sẽ có sau nỗ lực bổ sung trong quá trình vectorthực hiện.
Thảm Petersson

0

Về VLAs so với Vector

Bạn có nghĩ rằng một Vector có thể tận dụng lợi thế của chính VLAs. Không có VLAs, Vector phải chỉ định một số "thang" nhất định của mảng, ví dụ 10, 100, 10000 để lưu trữ để cuối cùng bạn phân bổ một mảng 10000 vật phẩm để chứa 101 vật phẩm. Với VLAs, nếu bạn thay đổi kích thước thành 200, thuật toán có thể cho rằng bạn sẽ chỉ cần 200 và có thể phân bổ một mảng 200 mặt hàng. Hoặc nó có thể phân bổ một bộ đệm nói n * 1.5.

Dù sao, tôi lập luận rằng nếu bạn biết bạn cần bao nhiêu vật phẩm trong thời gian chạy, thì một VLA có hiệu suất cao hơn (như tiêu chuẩn của Mats đã thể hiện). Những gì ông đã chứng minh là một lần lặp hai lần đơn giản. Hãy nghĩ về mô phỏng monte carlo trong đó các mẫu ngẫu nhiên được thực hiện lặp đi lặp lại hoặc thao tác hình ảnh (như bộ lọc Photoshop) trong đó việc tính toán được thực hiện trên mỗi phần tử nhiều lần và hoàn toàn có thể mỗi tính toán trên mỗi phần tử liên quan đến việc nhìn vào hàng xóm.

Con trỏ thêm đó nhảy từ vectơ đến mảng bên trong của nó cộng lại.

Trả lời câu hỏi chính

Nhưng khi bạn nói về việc sử dụng cấu trúc được phân bổ động như LinkedList, không có so sánh. Một mảng cung cấp truy cập trực tiếp bằng cách sử dụng số học con trỏ đến các phần tử của nó. Sử dụng một danh sách được liên kết, bạn phải đi bộ các nút để đến một yếu tố cụ thể. Vì vậy, VLA chiến thắng trong kịch bản này.

Theo câu trả lời này , nó phụ thuộc vào kiến ​​trúc, nhưng trong một số trường hợp, việc truy cập bộ nhớ trên ngăn xếp sẽ nhanh hơn do ngăn xếp có sẵn trên bộ đệm. Với một số lượng lớn các yếu tố, điều này có thể bị phủ nhận (có khả năng là nguyên nhân của lợi nhuận giảm dần mà Mats thấy trong điểm chuẩn của mình). Tuy nhiên, điều đáng chú ý là kích thước Cache đang tăng lên đáng kể và bạn có thể sẽ thấy số lượng đó tăng lên tương ứng.


Tôi không chắc là tôi hiểu tài liệu tham khảo của bạn cho các danh sách được liên kết, vì vậy tôi đã thêm một phần vào câu hỏi, giải thích bối cảnh thêm một chút và thêm các ví dụ về các lựa chọn thay thế mà tôi nghĩ đến.
hyde

Tại sao một std::vectornhu cầu quy mô của mảng? Tại sao nó cần không gian cho các yếu tố 10K khi nó chỉ cần 101? Ngoài ra, câu hỏi không bao giờ đề cập đến các danh sách được liên kết, vì vậy tôi không chắc bạn đã lấy nó từ đâu. Cuối cùng, các VLA trong C99 được phân bổ theo ngăn xếp; chúng là một hình thức tiêu chuẩn của alloca(). Bất cứ điều gì yêu cầu lưu trữ heap (nó tồn tại xung quanh sau khi hàm trả về) hoặc một realloc()(mảng tự thay đổi kích thước) sẽ cấm VLAs.
chrisaycock

@chrisaycock C ++ thiếu hàm realloc () vì một số lý do, giả sử bộ nhớ được cấp phát mới []. Không phải đó là lý do chính tại sao std :: vector phải sử dụng tỷ lệ?

@Lundin C ++ có chia tỷ lệ vectơ theo lũy thừa mười không? Tôi chỉ có ấn tượng rằng Mike Brown thực sự bối rối trước câu hỏi, đưa ra tham chiếu danh sách liên kết. (Anh ấy cũng đã đưa ra một khẳng định trước đó ngụ ý C99 VLAs sống trên đống.)
chayaycock

@hyde Tôi không nhận ra đó là những gì bạn đang nói. Tôi nghĩ bạn có nghĩa là cấu trúc dữ liệu dựa trên đống khác. Thật thú vị khi bạn đã thêm phần làm rõ này. Tôi không đủ đam mê để nói với bạn về sự khác biệt giữa chúng.
Michael Brown

0

Lý do để sử dụng VLA chủ yếu là hiệu suất. Đó là một sai lầm khi coi thường ví dụ wiki là chỉ có một sự khác biệt "không liên quan". Tôi có thể dễ dàng thấy các trường hợp mà chính xác mã đó có thể có một sự khác biệt rất lớn, ví dụ, nếu hàm đó được gọi trong một vòng lặp chặt chẽ, read_valthì hàm IO trở lại rất nhanh trên một loại hệ thống có tốc độ rất quan trọng.

Trên thực tế, ở hầu hết các nơi sử dụng VLAs theo cách này, họ không thay thế các cuộc gọi heap mà thay vào đó thay thế một số thứ như:

float vals[256]; /* I hope we never get more! */

Điều về bất kỳ tuyên bố địa phương là nó cực kỳ nhanh chóng. Dòng float vals[n]thường chỉ yêu cầu một vài hướng dẫn của bộ xử lý (có thể chỉ là một.) Nó chỉ đơn giản là thêm giá trị vào ncon trỏ ngăn xếp.

Mặt khác, phân bổ heap yêu cầu đi bộ cấu trúc dữ liệu để tìm một khu vực miễn phí. Thời gian có lẽ là một thứ tự cường độ dài hơn ngay cả trong trường hợp may mắn nhất. (Tức là chỉ hành động đặt nlên ngăn xếp và gọi malloccó lẽ là 5-10 hướng dẫn.) Có lẽ tồi tệ hơn nhiều nếu có bất kỳ lượng dữ liệu hợp lý nào trong đống. Tôi sẽ không ngạc nhiên khi thấy một trường hợp mallocchậm hơn 100 lần đến 1000 lần trong một chương trình thực sự.

Tất nhiên, sau đó bạn cũng có một số tác động hiệu suất với kết hợp free, có thể tương tự như cường độ của malloccuộc gọi.

Ngoài ra, có vấn đề phân mảnh bộ nhớ. Rất nhiều phân bổ nhỏ có xu hướng phân mảnh đống. Phân mảnh đống cả bộ nhớ lãng phí và tăng thời gian cần thiết để phân bổ bộ nhớ.


Về ví dụ Wikipedia: nó có thể là một phần của một ví dụ hay, nhưng không có ngữ cảnh, nhiều mã hơn xung quanh nó, nó không thực sự hiển thị bất kỳ điều gì trong 5 điều được liệt kê trong câu hỏi của tôi. Nếu không, tôi đồng ý với lời giải thích của bạn. Mặc dù có một điều cần lưu ý: sử dụng VLAs có thể có chi phí truy cập các biến cục bộ, nhưng bù lại tất cả các biến cục bộ không nhất thiết phải biết vào thời gian biên dịch, do đó, cần phải cẩn thận để không thay thế chi phí heap một lần bằng một hình phạt vòng lặp bên trong cho mỗi lần lặp.
hyde

Ừm ... không chắc ý của bạn là gì. Khai báo biến cục bộ là một hoạt động đơn lẻ và bất kỳ trình biên dịch được tối ưu hóa nhẹ nào cũng sẽ kéo phân bổ ra khỏi một vòng lặp bên trong. Không có "chi phí" cụ thể nào trong việc truy cập các biến cục bộ, chắc chắn không phải là một VLA sẽ tăng.
Gort Robot

Ví dụ cụ thể :: int vla[n]; if(test()) { struct LargeStruct s; int i; }offset offset của ssẽ không được biết tại thời điểm biên dịch, và cũng có nghi ngờ nếu trình biên dịch sẽ di chuyển lưu trữ ira khỏi phạm vi bên trong sang offset stack cố định. Vì vậy, mã máy bổ sung là cần thiết bởi vì không xác định, và điều này cũng có thể ăn hết các thanh ghi, quan trọng trên phần cứng PC. Nếu bạn muốn mã ví dụ bao gồm đầu ra lắp ráp trình biên dịch, vui lòng đặt một câu hỏi riêng;)
hyde 15/03/13

Trình biên dịch này không phải phân bổ theo thứ tự gặp phải trong mã và không có vấn đề gì nếu không gian được phân bổ và không được sử dụng. Trình tối ưu hóa thông minh sẽ phân bổ không gian cho sikhi chức năng được nhập, trước đó testđược gọi hoặc vlađược phân bổ, vì phân bổ cho sikhông có tác dụng phụ. (Và, trên thực tế, ithậm chí có thể được đặt trong một thanh ghi, nghĩa là hoàn toàn không có "phân bổ".) Không có trình biên dịch nào đảm bảo thứ tự phân bổ trên ngăn xếp, hoặc thậm chí là ngăn xếp được sử dụng.
Gort Robot

(đã xóa một nhận xét sai do sai lầm ngu ngốc)
hyde 15/03/13
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.