ตัวอย่าง Array ความยาวตัวแปร C ที่ดี [ปิด]


9

คำถามนี้ค่อนข้างได้รับการแช่แข็งที่ดังนั้นฉันจึงตัดสินใจลบที่นั่นและลองที่นี่แทน หากคุณคิดว่ามันไม่พอดีที่นี่อย่างน้อยโปรดแสดงความคิดเห็นเกี่ยวกับคำแนะนำวิธีการหาตัวอย่างฉันหลังจาก ...

คุณสามารถยกตัวอย่างได้หรือไม่ว่าการใช้ C99 VLA ให้ประโยชน์ที่เหนือกว่าบางอย่างเช่นฮีปมาตรฐานที่ใช้กลไก C ++ RAII ปัจจุบันหรือไม่

ตัวอย่างหลังจากฉันควร:

  1. บรรลุข้อได้เปรียบด้านประสิทธิภาพที่วัดได้ง่าย (10%) จากการใช้ heap
  2. ไม่มีวิธีแก้ปัญหาที่ดีซึ่งไม่จำเป็นต้องใช้ทั้งชุดเลย
  3. ประโยชน์ที่แท้จริงจากการใช้ขนาดไดนามิกแทนที่จะเป็นขนาดสูงสุดคงที่
  4. ไม่น่าจะทำให้เกิดการล้นสแต็คในสถานการณ์การใช้งานปกติ
  5. แข็งแรงพอที่จะล่อลวงนักพัฒนาที่ต้องการประสิทธิภาพในการรวมไฟล์ต้นฉบับ C99 ในโครงการ C ++

การเพิ่มความกระจ่างเกี่ยวกับบริบท: ฉันหมายถึง VLA ตามความหมายโดย C99 และไม่รวมอยู่ในมาตรฐาน C ++: int array[n]โดยที่nเป็นตัวแปร และฉันเป็นตัวอย่างของกรณีการใช้งานที่มันสำคัญกว่าทางเลือกอื่น ๆ ที่เสนอโดยมาตรฐานอื่น ๆ (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

ความคิดบางอย่าง:

  • ฟังก์ชันรับค่า varargs ซึ่ง จำกัด การนับรายการเป็นสิ่งที่สมเหตุสมผล แต่ไม่มีข้อ จำกัด ด้านบนระดับ API ที่มีประโยชน์
  • ฟังก์ชั่นวนซ้ำโดยที่กองซ้อนที่สูญเปล่าไม่พึงประสงค์
  • การจัดสรรและการเผยแพร่ขนาดเล็กจำนวนมากซึ่งค่าใช้จ่ายในกองจะไม่ดี
  • การจัดการอาเรย์หลายมิติ (เช่นเมทริกซ์ขนาดโดยพลการ) ซึ่งประสิทธิภาพมีความสำคัญและคาดว่าจะมีฟังก์ชั่นขนาดเล็กจำนวนมาก
  • จากความคิดเห็น: อัลกอริทึมพร้อมกันที่จัดสรรกองมีค่าใช้จ่ายในการประสาน

วิกิพีเดียมีตัวอย่างที่ไม่ตรงตามเกณฑ์ของฉันเพราะความแตกต่างในทางปฏิบัติในการใช้กองดูอย่างน้อยก็ไม่เกี่ยวข้องกับบริบท นอกจากนี้ยังไม่เหมาะเนื่องจากไม่มีบริบทเพิ่มเติมดูเหมือนว่าการนับรายการอาจทำให้เกิดการล้นสแต็คได้เป็นอย่างดี

หมายเหตุ: ฉันเฉพาะรหัสตัวอย่างหรือข้อเสนอแนะของอัลกอริทึมที่จะได้รับประโยชน์จากสิ่งนี้สำหรับฉันที่จะใช้ตัวอย่างด้วยตนเอง


1
บิตเก็งกำไร (ตั้งแต่นี้เป็นค้อนมองหาเล็บ) แต่บางทีอาจalloca()จะจริงๆ Outshine malloc()ในสภาพแวดล้อมแบบมัลติเธรดเพราะการต่อสู้ล็อคในภายหลัง แต่นี่เป็นการยืดตัวจริงเนื่องจากอาร์เรย์ขนาดเล็กควรใช้ขนาดคงที่และอาร์เรย์ขนาดใหญ่อาจต้องการฮีปอยู่ดี
chrisaycock

1
@ chrisaycock ใช่ค้อนมากกำลังมองหาเล็บ แต่ค้อนที่มีอยู่จริง (ไม่ว่าจะเป็น C99 VLA หรือมาตรฐานที่ไม่จริงallocaซึ่งฉันคิดว่าเป็นสิ่งเดียวกัน) แต่สิ่งที่มีหลายเธรดเป็นสิ่งที่ดีคำถามแก้ไขเพื่อรวมไว้!
hyde

ข้อเสียอย่างหนึ่งของ VLA คือไม่มีกลไกในการตรวจสอบความล้มเหลวในการจัดสรร หากมีหน่วยความจำไม่เพียงพอพฤติกรรมจะไม่ได้กำหนด (เช่นเดียวกับจริงสำหรับอาร์เรย์ขนาดคงที่ - และสำหรับ alloca ())
Keith Thompson

@ KeithThompson ไม่รับประกันว่า malloc / ใหม่ตรวจจับความล้มเหลวในการจัดสรรตัวอย่างเช่นดู Notes สำหรับหน้า man Linux malloc ( linux.die.net/man/3/malloc )
hyde

@hyde: และมันก็เป็นที่ถกเถียงกันอยู่ว่าmallocพฤติกรรมของ Linux นั้นสอดคล้องกับมาตรฐาน C หรือไม่
Keith Thompson

คำตอบ:


9

ฉันเพิ่งแฮ็กโปรแกรมเล็ก ๆ ที่สร้างชุดของตัวเลขสุ่มเริ่มใหม่ที่เมล็ดเดียวกันในแต่ละครั้งเพื่อให้แน่ใจว่ามัน "ยุติธรรม" และ "เปรียบได้" เมื่อไปพร้อมกันมันจะหาค่าต่ำสุดและสูงสุดของค่าเหล่านี้ และเมื่อมันได้สร้างชุดของตัวเลขที่จะนับจำนวนที่จะสูงกว่าค่าเฉลี่ยของและminmax

std::vector<>สำหรับอาร์เรย์ขนาดเล็กมากก็แสดงให้เห็นถึงผลประโยชน์ที่ชัดเจนกว่า VLA ของ

มันไม่ใช่ปัญหาจริง แต่เราสามารถจินตนาการได้อย่างง่ายดายว่าเรากำลังอ่านค่าจากไฟล์เล็ก ๆ แทนที่จะใช้ตัวเลขสุ่มและทำการคำนวณอื่น ๆ การนับ / นาที / สูงสุดที่มีความหมายมากขึ้นด้วยรหัสประเภทเดียวกัน .

สำหรับค่าที่น้อยมากของ "จำนวนตัวเลขสุ่ม" (x) ในฟังก์ชั่นที่เกี่ยวข้องการvlaแก้ปัญหาจะชนะโดยกำไรที่มาก เมื่อขนาดใหญ่ขึ้นการชนะจะเล็กลงและขนาดที่เพียงพอโซลูชันเวกเตอร์ดูเหมือนจะมีประสิทธิภาพมากขึ้น - ไม่ได้ศึกษาตัวแปรนั้นมากเกินไปเมื่อเราเริ่มมีองค์ประกอบหลายพันองค์ประกอบใน VLA ไม่ใช่ จริง ๆ แล้วสิ่งที่พวกเขาตั้งใจจะทำ ...

และฉันแน่ใจว่ามีบางคนจะบอกฉันว่ามีวิธีการเขียนโค้ดทั้งหมดนี้ด้วยเทมเพลตจำนวนหนึ่งและทำให้มันทำงานได้โดยไม่ต้องทำงานมากกว่า RDTSC และcoutบิตที่รันไทม์ ... แต่ฉันไม่คิดว่าจริง ๆ แล้ว ประเด็น

เมื่อใช้ตัวแปรเฉพาะนี้ฉันได้รับความแตกต่างประมาณ 10% ระหว่างfunc1(VLA) และ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

นี้รวบรวมด้วย: g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

นี่คือรหัส:

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

ว้าว, ระบบแสดงของฉันการปรับปรุง 30% ในรุ่น VLA std::vectorมากกว่า
chrisaycock

1
ลองใช้ขนาดช่วงประมาณ 5-15 แทนที่จะเป็น 20-200 และคุณอาจมีการปรับปรุง 1000% หรือมากกว่านั้น [ขึ้นอยู่กับตัวเลือกของคอมไพเลอร์ด้วย - ฉันจะแก้ไขโค้ดด้านบนเพื่อแสดงตัวเลือกของคอมไพเลอร์ใน gcc]
Mats Petersson

ฉันเพียงแค่เพิ่มfunc3ซึ่งใช้v.push_back(rand())แทนและขจัดความจำเป็นในการv[i] = rand(); resize()มันต้องใช้เวลาประมาณ 10% resize()อีกต่อไปเมื่อเทียบกับหนึ่งโดยใช้ [แน่นอนในกระบวนการฉันพบว่าการใช้งานv[i]เป็นตัวช่วยสำคัญในเวลาที่ฟังก์ชั่นใช้ - ฉันแปลกใจเล็กน้อยเกี่ยวกับเรื่องนั้น]
Mats Petersson

1
@MikeBrown คุณรู้จักการstd::vectorใช้งานจริงซึ่งจะใช้ VLA / allocaหรือว่าเป็นการเก็งกำไร?
hyde

3
เวกเตอร์ใช้อาร์เรย์จริง ๆ แต่เท่าที่ฉันเข้าใจมันไม่มีวิธีใช้ VLA ฉันเชื่อว่าตัวอย่างของฉันแสดงให้เห็นว่า VLA นั้นมีประโยชน์ในบางกรณี (อาจเป็นมาก) ในกรณีที่ปริมาณข้อมูลมีน้อย แม้ว่าเวกเตอร์นั้นจะเป็น VLA มันก็จะเป็นความพยายามเพิ่มเติมในvectorการนำไปใช้
Mats Petersson

0

เกี่ยวกับ VLAs กับ Vector

คุณคิดว่า Vector สามารถใช้ประโยชน์จาก VLAs ได้หรือไม่ หากไม่มี VLAs Vector จะต้องระบุ "สเกล" ของอาร์เรย์บางตัวเช่น 10, 100, 10000 สำหรับการจัดเก็บดังนั้นคุณจะต้องจัดสรรอาร์เรย์ไอเท็ม 10,000 รายการเพื่อเก็บ 101 รายการ ด้วย VLA หากคุณปรับขนาดเป็น 200 อัลกอริทึมอาจคิดว่าคุณต้องการเพียง 200 และสามารถจัดสรรอาร์เรย์ไอเท็มได้ 200 รายการ หรือสามารถจัดสรรบัฟเฟอร์ของ say n * 1.5

อย่างไรก็ตามฉันขอยืนยันว่าถ้าคุณรู้ว่ามีกี่ไอเท็มที่คุณต้องใช้ในการรันไทม์ VLA นั้นมีประสิทธิภาพมากกว่า สิ่งที่เขาแสดงให้เห็นคือการทำซ้ำสองครั้งง่ายๆ คิดว่าการจำลองมอนเต้คาร์โลที่สุ่มตัวอย่างถูกนำมาใช้ซ้ำ ๆ หรือการจัดการภาพ (เช่นตัวกรอง Photoshop) ที่การคำนวณจะทำในแต่ละองค์ประกอบหลายครั้งและอาจเป็นไปได้ที่การคำนวณในแต่ละองค์ประกอบเกี่ยวข้องกับการมองเพื่อนบ้าน

ตัวชี้พิเศษนั้นกระโดดจากเวกเตอร์ไปยังอาร์เรย์ภายใน

ตอบคำถามหลัก

แต่เมื่อคุณพูดถึงการใช้โครงสร้างที่จัดสรรแบบไดนามิกเช่น LinkedList จะไม่มีการเปรียบเทียบ อาร์เรย์จัดเตรียมการเข้าถึงโดยตรงโดยใช้ตัวชี้ทางคณิตศาสตร์ไปยังองค์ประกอบ ใช้รายการเชื่อมโยงคุณต้องเดินโหนดเพื่อไปยังองค์ประกอบเฉพาะ ดังนั้น VLA จึงชนะในสถานการณ์นี้

ตามคำตอบนี้มันขึ้นอยู่กับสถาปัตยกรรม แต่ในบางกรณีการเข้าถึงหน่วยความจำบนสแต็กจะเร็วขึ้นเนื่องจากสแต็กมีอยู่ในแคช ด้วยองค์ประกอบจำนวนมากสิ่งนี้อาจถูกทำให้เป็นโมฆะ (อาจเป็นสาเหตุของการลดลงของผลตอบแทนที่เสื่อเห็นในมาตรฐานของเขา) อย่างไรก็ตามเป็นที่น่าสังเกตว่าขนาดแคชนั้นเพิ่มขึ้นอย่างมากและคุณอาจเห็นจำนวนนั้นเพิ่มขึ้นตามไปด้วย


ฉันไม่แน่ใจว่าฉันเข้าใจการอ้างอิงของคุณไปยังรายการที่เชื่อมโยงหรือไม่ดังนั้นฉันจึงเพิ่มหัวข้อคำถามอธิบายบริบทเพิ่มเติมอีกเล็กน้อยและเพิ่มตัวอย่างของทางเลือกที่ฉันคิด
hyde

ทำไมstd::vectorต้องมีตาชั่งอาร์เรย์ ทำไมมันต้องการพื้นที่สำหรับองค์ประกอบ 10K เมื่อต้องการเพียง 101? นอกจากนี้คำถามไม่ได้กล่าวถึงรายการที่เชื่อมโยงดังนั้นฉันไม่แน่ใจว่าคุณได้รับจากที่ใด ในที่สุด VLA ใน C99 จะถูกจัดสรรแบบสแต็ก alloca()พวกเขาเป็นรูปแบบมาตรฐานของ สิ่งใดก็ตามที่ต้องใช้หน่วยเก็บฮีป (มันอาศัยอยู่หลังจากฟังก์ชันส่งคืน) หรือ a realloc()(อาร์เรย์ปรับขนาดตัวเอง) จะห้าม VLAs อยู่ดี
chrisaycock

@chrisaycock C ++ ขาดฟังก์ชั่น realloc () ด้วยเหตุผลบางอย่างสมมติว่ามีการจัดสรรหน่วยความจำด้วย [ใหม่] นั่นไม่ใช่เหตุผลหลักที่ทำไม std :: vector ต้องใช้สเกล?

@Lundin C ++ ปรับขนาดเวกเตอร์ตามกำลังสิบหรือไม่ ฉันเพิ่งได้รับความประทับใจที่คำถามของ Mike Brown สับสนจริง ๆ เนื่องจากมีการอ้างอิงรายการเชื่อมโยง (นอกจากนี้เขายังยืนยันก่อนหน้านี้ที่บอกเป็นนัยถึง C99 VLAs อาศัยอยู่บนกอง)
chrisaycock

@hyde ฉันไม่ทราบว่าเป็นสิ่งที่คุณพูดถึง ฉันคิดว่าคุณหมายถึงโครงสร้างข้อมูลตามกองอื่น ๆ น่าสนใจตอนนี้ที่คุณเพิ่มคำชี้แจงนี้ ฉันไม่เพียงพอที่จะบอกคุณถึงความแตกต่างระหว่าง C ++
Michael Brown

0

เหตุผลที่ใช้ VLA คือประสิทธิภาพเป็นหลัก มันเป็นความผิดพลาดที่ไม่สนใจตัวอย่างของวิกิว่ามีความแตกต่าง "ไม่เกี่ยวข้อง" ฉันสามารถดูกรณีที่รหัสนั้นอาจมีความแตกต่างอย่างมากตัวอย่างเช่นถ้าฟังก์ชันนั้นถูกเรียกในการวนรอบที่แน่นหนาซึ่งread_valเป็นฟังก์ชัน IO ที่ส่งคืนอย่างรวดเร็วในระบบบางประเภทที่ความเร็วมีความสำคัญ

ในความเป็นจริงในสถานที่ส่วนใหญ่ที่มีการใช้ VLAs ในลักษณะนี้พวกเขาไม่ได้แทนที่การเรียกฮีป แต่แทนการแทนที่:

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

สิ่งที่เกี่ยวกับการประกาศใด ๆ ในประเทศคือว่ามันเป็นอย่างมากอย่างรวดเร็ว บรรทัดfloat vals[n]โดยทั่วไปต้องการเพียงสองสามตัวประมวลผลคำสั่ง (อาจเป็นเพียงหนึ่ง) มันเพียงเพิ่มค่าในnตัวชี้สแต็ค

ในทางกลับกันการจัดสรรฮีปต้องใช้โครงสร้างข้อมูลเพื่อค้นหาพื้นที่ว่าง เวลาอาจเป็นลำดับความสำคัญอีกต่อไปแม้ในกรณีที่โชคดีที่สุด (เช่นการวางลงnบนสแต็กและการโทรmallocอาจเป็นคำสั่ง 5-10 ข้อ) อาจแย่กว่านั้นถ้ามีข้อมูลจำนวนหนึ่งในกอง มันจะไม่ทำให้ฉันประหลาดใจเลยเมื่อเห็นกรณีที่mallocช้ากว่า 100x ถึง 1,000x ในโปรแกรมจริง

แน่นอนว่าคุณมีผลกระทบต่อประสิทธิภาพการทำงานด้วยการจับคู่freeอาจคล้ายกับขนาดของการmallocโทร

นอกจากนี้ยังมีปัญหาการกระจายตัวของหน่วยความจำ การจัดสรรเล็กน้อยจำนวนมากมีแนวโน้มที่จะแยกส่วนของฮีป แยกส่วนของฮีปทั้งหน่วยความจำเหลือและเพิ่มเวลาที่จำเป็นในการจัดสรรหน่วยความจำ


เกี่ยวกับตัวอย่างวิกิพีเดีย: มันอาจเป็นส่วนหนึ่งของตัวอย่างที่ดี แต่หากไม่มีบริบทและมีโค้ดมากกว่านั้นก็ไม่ได้แสดง 5 สิ่งใด ๆ ที่แจกแจงในคำถามของฉัน มิฉะนั้นฉันเห็นด้วยกับคำอธิบายของคุณ แม้ว่าสิ่งหนึ่งที่ต้องคำนึงถึง: การใช้ VLAs อาจมีค่าใช้จ่ายในการเข้าถึงตัวแปรท้องถิ่นโดยที่ไม่จำเป็นต้องทราบถึงการรวมตัวแปรท้องถิ่นทั้งหมดในเวลาที่รวบรวมดังนั้นจึงต้องใช้ความระมัดระวังเพื่อไม่ให้แทนที่ต้นทุนฮีปครั้งเดียวด้วย โทษวงในสำหรับการวนซ้ำทุกครั้ง
ไฮด์

อืม ... ไม่แน่ใจว่าคุณหมายถึงอะไร การประกาศตัวแปรโลคัลเป็นการดำเนินการครั้งเดียวและคอมไพเลอร์ที่ได้รับการปรับให้เหมาะสมจะดึงการจัดสรรออกจากลูปด้านใน ไม่มีค่าใช้จ่าย "พิเศษ" ในการเข้าถึงตัวแปรท้องถิ่นแน่นอนว่าไม่ใช่ VLA ที่จะเพิ่มขึ้น
Gort the Robot

ตัวอย่างที่เป็นรูปธรรมint vla[n]; if(test()) { struct LargeStruct s; int i; }:: สแต็กออฟของsจะไม่เป็นที่รู้จักในเวลารวบรวมและเป็นที่น่าสงสัยว่าคอมไพเลอร์จะย้ายที่เก็บข้อมูลiออกจากขอบเขตด้านในไปยังสแต็กออฟเซ็ตคงที่ ดังนั้นจึงจำเป็นต้องมีรหัสเครื่องเพิ่มเติมเนื่องจากการใช้ทางอ้อมและสิ่งนี้อาจทำให้หมดความสำคัญในการใช้ฮาร์ดแวร์พีซี หากคุณต้องการโค้ดตัวอย่างประกอบกับการส่งออกคอมไพเลอร์รวมโปรดถามคำถามที่แยกต่างหาก)
hyde

คอมไพเลอร์ไม่จำเป็นต้องจัดสรรตามลำดับที่พบในโค้ดและไม่สำคัญว่าจะมีการจัดสรรพื้นที่และไม่ได้ใช้หรือไม่ เครื่องมือเพิ่มประสิทธิภาพสมาร์ทจะจัดสรรพื้นที่สำหรับsและiเมื่อมีการป้อนฟังก์ชั่นก่อนที่จะtestถูกเรียกหรือvlaได้รับการจัดสรรเป็นการจัดสรรsและiไม่มีผลข้างเคียง (และในความเป็นจริงiอาจถูกวางไว้ในทะเบียนซึ่งหมายความว่าไม่มี "การจัดสรร" เลย) ไม่มีคอมไพเลอร์รับประกันคำสั่งของการจัดสรรบนสแต็กหรือแม้แต่สแต็กที่ใช้
Gort the Robot

(ลบความคิดเห็นที่ผิดเนื่องจากข้อผิดพลาดที่โง่)
hyde
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.