อาร์เรย์หรือ Malloc?


13

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

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

2
เป็นข้อสันนิษฐานว่ารหัสมีการกำหนดเป้าหมายสำหรับสภาพแวดล้อมที่ไม่ได้ฝังอยู่หรือไม่?
tehnyit

คำตอบ:


28

ข้อแตกต่างที่สำคัญคือ VLAs (อาร์เรย์ความยาวผันแปร) ไม่ได้จัดเตรียมกลไกสำหรับตรวจจับความล้มเหลวในการจัดสรร

ถ้าคุณประกาศ

char result[len];

และlenเกินจำนวนพื้นที่สแต็กที่มีอยู่พฤติกรรมของโปรแกรมของคุณจะไม่ได้กำหนด ไม่มีกลไกภาษาทั้งในการพิจารณาล่วงหน้าว่าการจัดสรรจะประสบความสำเร็จหรือสำหรับการพิจารณาหลังจากข้อเท็จจริงว่ามันจะประสบความสำเร็จ

ในทางตรงกันข้ามถ้าคุณเขียน:

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

จากนั้นคุณสามารถจัดการกับความล้มเหลวได้อย่างสง่างามหรืออย่างน้อยรับประกันว่าโปรแกรมของคุณจะไม่พยายามดำเนินการต่อหลังจากเกิดความล้มเหลว

(ส่วนใหญ่แล้วบนระบบ Linux malloc()สามารถจัดสรรพื้นที่ที่อยู่ได้แม้ว่าจะไม่มีหน่วยความจำที่เกี่ยวข้องอยู่ แต่ความพยายามที่จะใช้พื้นที่นั้นในภายหลังสามารถเรียกใช้OOM Killerได้ แต่การตรวจสอบmalloc()ความล้มเหลวยังคงเป็นวิธีปฏิบัติที่ดี)

อีกปัญหาหนึ่งในหลาย ๆ ระบบคือมีพื้นที่มากขึ้น (อาจมีพื้นที่มากขึ้น) malloc()สำหรับวัตถุอัตโนมัติเช่น VLA

และตามคำตอบของฟิลิปที่กล่าวถึงไปแล้ว VLA ถูกเพิ่มเข้ามาใน C99 (โดยเฉพาะอย่างยิ่ง Microsoft ไม่รองรับพวกเขา)

และ VLA ถูกเลือกใช้ใน C11 คอมไพเลอร์ C11 ส่วนใหญ่น่าจะสนับสนุนพวกเขา แต่คุณไม่สามารถไว้ใจได้


14

อาร์เรย์อัตโนมัติที่มีความยาวผันแปรได้ถูกนำไปใช้กับ C ใน C99

นอกจากว่าคุณจะมีความกังวลเกี่ยวกับการเปรียบเทียบย้อนหลังกับมาตรฐานเก่ามันก็ดี

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


7
ฉันต้องไม่เห็นด้วยกับ "ถ้ามันใช้งานได้อย่าแตะมัน" dictum เชื่ออย่างไม่ถูกต้องว่ารหัสบางอย่าง "ทำงาน" อาจทำให้คุณต้องแก้ไขปัญหาในบางรหัสที่ "ทำงาน" ความเชื่อจะต้องถูกแทนที่ด้วยการยอมรับอย่างไม่แน่นอนว่าบางรหัสทำงานได้ในขณะนี้
Bruce Ediger

2
อย่าแตะต้องมันจนกว่าคุณจะสร้างเวอร์ชันที่อาจ "ได้ผล" ที่ดีกว่า ...
H_7

8

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


เกี่ยวกับสิ่งนี้ในฟังก์ชันที่ไม่ใช่ c99 (char []) {ถ่านผลลัพธ์ [sizeof (ถ่าน)] = ตัวอักษรบางตัว; ส่งผลผ่านเครือข่าย}
ถุง Dev

@DevBag char result [sizeof(char)]เป็นอาร์เรย์ของขนาด1(เพราะsizeof(char)เท่ากับหนึ่ง) some charsดังนั้นการกำหนดเป็นไปตัด
dasblinkenlight

ขอโทษด้วยฉันหมายถึงฟังก์ชั่นนี้ (char str []) {ถ่านผลลัพธ์ [sizeof (str)] = ตัวอักษรบางตัว; ส่งผลผ่านเครือข่าย}
ถุง Dev

4
@DevBag สิ่งนี้จะไม่ทำงานเช่นกัน - str จะลดลงไปที่ตัวชี้ดังนั้นมันsizeofจะเป็นสี่หรือแปดขึ้นอยู่กับขนาดตัวชี้ในระบบของคุณ
dasblinkenlight

2
หากคุณกำลังใช้รุ่น C ที่ไม่มีอาร์เรย์ความยาวผันแปรคุณอาจสามารถทำสิ่งchar* result = alloca(len);นี้ซึ่งจัดสรรไว้บนสแต็ก มันมีผลกระทบพื้นฐานเหมือนกัน (และปัญหาพื้นฐานเดียวกัน)
Gort the Robot

6

ฉันชอบความคิดที่ว่าคุณสามารถมีอาเรย์ที่ถูกจัดสรรแบบรันไทม์โดยไม่มีการแตกแฟรกเมนต์หน่วยความจำตัวชี้ห้อย ฯลฯ อย่างไรก็ตามคนอื่น ๆ ได้ชี้ให้เห็นว่าการจัดสรรรันไทม์นี้อาจล้มเหลวอย่างเงียบ ๆ ดังนั้นฉันจึงลองใช้ gcc 4.5.3 ในสภาพแวดล้อมทุบตี 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
}

ผลลัพธ์คือ:

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

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

ตามปกติ YMMV


1
+1 สิ่งนี้มีประโยชน์มาก: 3
Kokizzu

เป็นเรื่องที่ดีเสมอที่จะมีรหัสที่จะนำไปใช้กับการอ้างสิทธิ์ที่ผู้คนทำ! ขอบคุณ ^ _ ^
Musa Al-hassy

3

โดยทั่วไปแล้วการพูดสแต็กเป็นสถานที่ที่ง่ายที่สุดและดีที่สุดในการวางข้อมูลของคุณ

ฉันจะหลีกเลี่ยงปัญหาของ VLA เพียงแค่จัดสรรอาร์เรย์ที่ใหญ่ที่สุดที่คุณคาดหวัง

อย่างไรก็ตามมีบางกรณีเมื่อกองที่ดีที่สุดและยุ่งกับ malloc มีค่าความพยายาม

  1. เมื่อข้อมูลจำนวนมาก แต่แปรผัน ขนาดใหญ่ขึ้นอยู่กับสภาพแวดล้อมของคุณ> 1K สำหรับระบบฝังตัว> 10MB สำหรับเซิร์ฟเวอร์องค์กร
  2. เมื่อคุณต้องการให้ข้อมูลคงอยู่หลังจากที่คุณออกจากรูทีนเช่นถ้าคุณกลับตัวชี้ไปยังข้อมูลของคุณ การใช้
  3. การรวมกันของตัวชี้คงที่และ malloc () มักจะดีกว่าการกำหนดอาเรย์ขนาดใหญ่

3

ในการเขียนโปรแกรมแบบฝังตัวเรามักจะใช้อาร์เรย์แบบคงที่แทนที่จะเป็น malloc เมื่อการดำเนินการแบบ malloc และการดำเนินการฟรีเป็นประจำ เนื่องจากการขาดการจัดการหน่วยความจำในระบบฝังตัวการดำเนินการจัดสรรและการดำเนินการฟรีบ่อยครั้งจะทำให้เกิดการแยกส่วนหน่วยความจำ แต่เราควรใช้วิธีที่ยุ่งยากเช่นการกำหนดขนาดสูงสุดของอาเรย์และการใช้อาเรย์แบบสแตติกท้องถิ่น

หากแอปพลิเคชันของคุณทำงานใน Linux หรือ Windows จะไม่มีการใช้ array หรือ malloc จุดสำคัญอยู่ในที่ที่คุณใช้โครงสร้างวันที่และตรรกะโค้ดของคุณ


1

สิ่งที่ไม่มีใครพูดถึงคือตัวเลือกอาเรย์ความยาวแปรผันอาจจะเร็วกว่า malloc / free อย่างมากมายตั้งแต่การจัดสรร VLA เป็นเพียงกรณีของการปรับตัวชี้สแต็ก (ใน GCC อย่างน้อย)

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


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

@ Donal Performance เป็นการแลกเปลี่ยนความจำกับความเร็วเสมอ หากคุณกำลังจะทำการจัดสรรอาเรย์หลายเมกะไบต์คุณจะมีจุดอย่างไรก็ตามแม้สักสองสามกิโลไบต์ตราบใดที่ฟังก์ชั่นไม่เรียกซ้ำมันเป็นการเพิ่มประสิทธิภาพที่ดี
JeremyP

1

นี่เป็นวิธีแก้ปัญหา C ทั่วไปที่ฉันใช้สำหรับปัญหาซึ่งอาจช่วยได้ ซึ่งแตกต่างจาก VLAs มันไม่ได้เผชิญกับความเสี่ยงในทางปฏิบัติของกองล้นในกรณีทางพยาธิวิทยา

/// 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;
}

วิธีใช้ในกรณีของคุณ:

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

สิ่งนี้จะทำอย่างไรในกรณีข้างต้นจะใช้สแต็คถ้าสตริงพอดีกับ 512 ไบต์หรือน้อยกว่า มิฉะนั้นจะใช้การจัดสรรฮีป สิ่งนี้จะมีประโยชน์ถ้าพูด 99% ของเวลาสตริงควรมีขนาด 512 ไบต์หรือน้อยกว่า อย่างไรก็ตามสมมติว่ามีบางกรณีที่แปลกใหม่อย่างบ้าคลั่งที่คุณอาจต้องจัดการในกรณีที่สตริงเป็น 32 กิโลไบต์ที่ผู้ใช้หลับบนแป้นพิมพ์หรืออะไรทำนองนั้น สิ่งนี้ทำให้ทั้งสองสถานการณ์สามารถจัดการได้โดยไม่มีปัญหา

รุ่นที่เกิดขึ้นจริงที่ผมใช้ในการผลิตนอกจากนี้ยังมีรุ่นของตนเองreallocและcallocและอื่น ๆ เช่นเดียวกับมาตรฐานสอดคล้อง c ++ โครงสร้างข้อมูลที่สร้างขึ้นบนแนวคิดเดียวกัน แต่ฉันสกัดขั้นต่ำที่จำเป็นในการแสดงให้เห็นถึงแนวคิด

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

จริง ๆ แล้วฉันสร้างมันนานมาแล้วในการตอบสนองต่อสถานการณ์ใน codebase แบบเก่าโดยใช้ C89 ซึ่งอดีตทีมคิดว่าจะไม่เกิดขึ้นเมื่อผู้ใช้จัดการชื่อรายการที่มีชื่อยาวเกิน 2047 ตัวอักษร (บางทีเขาอาจเผลอหลับบนคีย์บอร์ดของเขา ) เพื่อนร่วมงานของฉันพยายามเพิ่มขนาดของอาร์เรย์ที่จัดสรรในที่ต่าง ๆ เป็น 16,384 เพื่อตอบสนอง ณ จุดที่ฉันคิดว่ามันน่าขันและแลกเปลี่ยนความเสี่ยงที่มากขึ้นของการโอเวอร์โฟลว์สแต็กเพื่อแลกเปลี่ยนความเสี่ยงที่น้อยกว่าของบัฟเฟอร์ล้น นี่เป็นวิธีการแก้ปัญหาที่ง่ายมากในการเชื่อมต่อเพื่อแก้ไขกรณีเหล่านั้นโดยเพิ่มโค้ดสองสามบรรทัด สิ่งนี้ทำให้กรณีทั่วไปได้รับการจัดการอย่างมีประสิทธิภาพมากและยังคงใช้กองซ้อนโดยไม่มีกรณีที่หายากบ้าคลั่งที่เรียกร้องให้ฮีปชนซอฟต์แวร์ล้มเหลว อย่างไรก็ตามฉัน' พบว่ามีประโยชน์ตั้งแต่นั้นมาแม้กระทั่งหลังจาก C99 เนื่องจาก VLA ยังไม่สามารถป้องกันเราจากกองล้น อันนี้สามารถ แต่ยังคงพูลจากสแต็กสำหรับการร้องขอการจัดสรรขนาดเล็ก


1

เรียกกองจะถูก จำกัด อยู่เสมอ บนระบบปฏิบัติการหลักเช่น Linux หรือ Windows ขีด จำกัด คือหนึ่งหรือสองสามเมกะไบต์ (และคุณสามารถหาวิธีเปลี่ยนได้) ด้วยแอ็พพลิเคชันแบบมัลติเธรดบางตัวอาจต่ำกว่า (เนื่องจากเธรดสามารถสร้างได้ด้วยสแต็กขนาดเล็ก) บนระบบฝังตัวอาจมีขนาดเล็กเพียงไม่กี่กิโลไบต์ กฎง่ายๆคือหลีกเลี่ยงเฟรมการโทรที่มีขนาดใหญ่กว่าสองสามกิโลไบต์

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

อย่างไรก็ตามการใช้การจัดสรรหน่วยความจำแบบไดนามิกด้วยตนเองC (เช่นcallocหรือmalloc&free ) มีข้อเสีย:

  • มันสามารถล้มเหลวและคุณควรจะเสมอทดสอบสำหรับความล้มเหลว (เช่นcallocหรือmallocกลับNULL)

  • มันจะช้าจัดสรร VLA ที่ประสบความสำเร็จใช้เวลาไม่กี่นาโนวินาทีที่ประสบความสำเร็จmallocจะต้องหลาย microseconds (ในกรณีที่ดีเพียงเศษเสี้ยวของวินาทีที่) หรือแม้กระทั่งมากขึ้น (ในกรณีพยาธิวิทยาที่เกี่ยวข้องกับการนวดมากขึ้น)

  • มันยากกว่าในการเขียนโค้ด: คุณสามารถทำได้freeเฉพาะเมื่อคุณแน่ใจว่าไม่มีการใช้โซนแหลม ในกรณีของคุณคุณอาจโทรทั้งสองcallocและfreeในรูทีนเดียวกัน

หากคุณรู้ว่าส่วนใหญ่เวลาของคุณresult (ชื่อที่แย่มากคุณไม่ควรส่งคืนที่อยู่ของตัวแปรอัตโนมัติ VLA ดังนั้นฉันจึงใช้ bufแทนresultด้านล่าง) มีขนาดเล็กที่คุณสามารถทำได้ในกรณีพิเศษเช่น

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

อย่างไรก็ตามโค้ดข้างต้นสามารถอ่านได้น้อยลงและอาจเป็นการปรับให้เหมาะสมก่อนเวลาอันควร อย่างไรก็ตามมันมีความแข็งแกร่งกว่า VLA บริสุทธิ์

PS บางระบบ (เช่นบางลีนุกซ์ลีนุกซ์เปิดใช้งานตามค่าเริ่มต้น) มีหน่วยความจำ overcommitment (ซึ่งทำให้mallocการชี้บางอย่างแม้ว่าหน่วยความจำไม่เพียงพอ) นี่เป็นคุณสมบัติที่ฉันไม่ชอบและมักจะปิดการใช้งานบนเครื่อง Linux ของฉัน

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