ฉันใช้รหัสต่อไปนี้ในแอปพลิเคชันของฉันและมันใช้ได้ดี แต่ฉันสงสัยว่ามันจะดีกว่าที่จะทำมันด้วย malloc หรือปล่อยให้มันเป็นอย่างนั้นหรือ
function (int len)
{
char result [len] = some chars;
send result over network
}
ฉันใช้รหัสต่อไปนี้ในแอปพลิเคชันของฉันและมันใช้ได้ดี แต่ฉันสงสัยว่ามันจะดีกว่าที่จะทำมันด้วย malloc หรือปล่อยให้มันเป็นอย่างนั้นหรือ
function (int len)
{
char result [len] = some chars;
send result over network
}
คำตอบ:
ข้อแตกต่างที่สำคัญคือ 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 ส่วนใหญ่น่าจะสนับสนุนพวกเขา แต่คุณไม่สามารถไว้ใจได้
อาร์เรย์อัตโนมัติที่มีความยาวผันแปรได้ถูกนำไปใช้กับ C ใน C99
นอกจากว่าคุณจะมีความกังวลเกี่ยวกับการเปรียบเทียบย้อนหลังกับมาตรฐานเก่ามันก็ดี
โดยทั่วไปถ้าใช้งานได้อย่าแตะต้องมัน อย่าปรับให้เหมาะสมก่อนเวลา ไม่ต้องกังวลกับการเพิ่มคุณสมบัติพิเศษหรือวิธีการทำสิ่งต่าง ๆ ที่ชาญฉลาดเพราะคุณมักจะไม่ใช้มัน ง่าย ๆ เข้าไว้.
หากคอมไพเลอร์ของคุณรองรับอาร์เรย์ที่มีความยาวผันแปรอันตรายเพียงอย่างเดียวคือล้นสแต็คในบางระบบเมื่อlen
มีขนาดใหญ่อย่างน่าขัน หากคุณรู้ว่าlen
จะไม่ใหญ่กว่าจำนวนที่กำหนดและคุณรู้ว่าสแต็กของคุณจะไม่ล้นแม้ที่ความยาวสูงสุดให้ปล่อยโค้ดตามที่เป็นอยู่ มิฉะนั้นเขียนมันด้วยและmalloc
free
char result [sizeof(char)]
เป็นอาร์เรย์ของขนาด1
(เพราะsizeof(char)
เท่ากับหนึ่ง) some chars
ดังนั้นการกำหนดเป็นไปตัด
str
จะลดลงไปที่ตัวชี้ดังนั้นมันsizeof
จะเป็นสี่หรือแปดขึ้นอยู่กับขนาดตัวชี้ในระบบของคุณ
char* result = alloca(len);
นี้ซึ่งจัดสรรไว้บนสแต็ก มันมีผลกระทบพื้นฐานเหมือนกัน (และปัญหาพื้นฐานเดียวกัน)
ฉันชอบความคิดที่ว่าคุณสามารถมีอาเรย์ที่ถูกจัดสรรแบบรันไทม์โดยไม่มีการแตกแฟรกเมนต์หน่วยความจำตัวชี้ห้อย ฯลฯ อย่างไรก็ตามคนอื่น ๆ ได้ชี้ให้เห็นว่าการจัดสรรรันไทม์นี้อาจล้มเหลวอย่างเงียบ ๆ ดังนั้นฉันจึงลองใช้ 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
โดยทั่วไปแล้วการพูดสแต็กเป็นสถานที่ที่ง่ายที่สุดและดีที่สุดในการวางข้อมูลของคุณ
ฉันจะหลีกเลี่ยงปัญหาของ VLA เพียงแค่จัดสรรอาร์เรย์ที่ใหญ่ที่สุดที่คุณคาดหวัง
อย่างไรก็ตามมีบางกรณีเมื่อกองที่ดีที่สุดและยุ่งกับ malloc มีค่าความพยายาม
ในการเขียนโปรแกรมแบบฝังตัวเรามักจะใช้อาร์เรย์แบบคงที่แทนที่จะเป็น malloc เมื่อการดำเนินการแบบ malloc และการดำเนินการฟรีเป็นประจำ เนื่องจากการขาดการจัดการหน่วยความจำในระบบฝังตัวการดำเนินการจัดสรรและการดำเนินการฟรีบ่อยครั้งจะทำให้เกิดการแยกส่วนหน่วยความจำ แต่เราควรใช้วิธีที่ยุ่งยากเช่นการกำหนดขนาดสูงสุดของอาเรย์และการใช้อาเรย์แบบสแตติกท้องถิ่น
หากแอปพลิเคชันของคุณทำงานใน Linux หรือ Windows จะไม่มีการใช้ array หรือ malloc จุดสำคัญอยู่ในที่ที่คุณใช้โครงสร้างวันที่และตรรกะโค้ดของคุณ
สิ่งที่ไม่มีใครพูดถึงคือตัวเลือกอาเรย์ความยาวแปรผันอาจจะเร็วกว่า malloc / free อย่างมากมายตั้งแต่การจัดสรร VLA เป็นเพียงกรณีของการปรับตัวชี้สแต็ก (ใน GCC อย่างน้อย)
ดังนั้นหากฟังก์ชั่นนี้เป็นฟังก์ชั่นที่เรียกว่าบ่อยครั้ง (ซึ่งแน่นอนว่าคุณจะกำหนดโดยการทำโปรไฟล์) VLA เป็นตัวเลือกการเพิ่มประสิทธิภาพที่ดี
นี่เป็นวิธีแก้ปัญหา 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 ยังไม่สามารถป้องกันเราจากกองล้น อันนี้สามารถ แต่ยังคงพูลจากสแต็กสำหรับการร้องขอการจัดสรรขนาดเล็ก
เรียกกองจะถูก จำกัด อยู่เสมอ บนระบบปฏิบัติการหลักเช่น 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 ของฉัน