เหตุใดสตริงจึงช้า


23

นับตั้งแต่ชั้นเรียนการเขียนโปรแกรมครั้งแรกของฉันในโรงเรียนมัธยมฉันได้ยินมาว่าการทำงานของสายอักขระนั้นช้ากว่า - มีค่าใช้จ่ายสูงกว่า "การปฏิบัติการโดยเฉลี่ย" ในตำนาน ทำไมจึงทำให้ช้า (คำถามนี้ปล่อยให้กว้างโดยเจตนา)


11
หากคุณรู้ว่า "การดำเนินงานโดยเฉลี่ย" เหล่านี้เป็นสิ่งที่มีอยู่ในตำนานอย่างน้อยคุณจะบอกเราได้ไหมว่าอะไรบ้าง เนื่องจากคุณกำลังถามคำถามที่คลุมเครือนั้นยากที่จะเชื่อใจในการยืนยันของคุณว่าการดำเนินการที่ไม่ระบุรายละเอียดเหล่านี้เป็นเรื่องจริง
seh

1
@seh น่าเสียดายจริง ๆ ฉันไม่สามารถตอบได้ ไม่กี่ครั้งที่ฉันถามคนอื่นว่าสายช้ากว่าพวกเขาแค่ยักและพูดว่า "พวกเขาช้า" นอกจากนี้หากฉันมีข้อมูลที่เฉพาะเจาะจงมากขึ้นนี่จะเป็นคำถามสำหรับ SO ไม่ใช่โปรแกรมเมอร์ มันเป็นแนวเขตแดนอยู่แล้ว
ปรากฏ

ประเด็นคืออะไร? หากสตริงที่บอกว่าช้าจริง ๆ คุณจะหยุดใช้มันหรือไม่?
Tulains Córdova

ลืมมันไปเถอะ ถ้ามีคนบอกคุณเรื่องไร้สาระเช่นนั้นการโต้แย้งคือ: "จริงเหรอ? พวกเราควรใช้ int-array หรือไม่?"
Ingo

คำตอบ:


47

"การดำเนินการเฉลี่ย" เกิดขึ้นกับพื้นฐาน แต่แม้ในภาษาที่มีการใช้งานสตริงเป็นพื้นฐานพวกเขายังคงอยู่ภายใต้ประทุนและการทำทุกอย่างที่เกี่ยวข้องกับสตริงทั้งหมดต้องใช้เวลา O (N) โดยที่ N คือความยาวของสตริง

ตัวอย่างเช่นการเพิ่มตัวเลขสองหลักโดยทั่วไปจะใช้คำสั่ง 2-4 ASM การต่อสองสตริง ("เพิ่ม") จำเป็นต้องมีการจัดสรรหน่วยความจำใหม่และสำเนาหนึ่งหรือสองสตริงที่เกี่ยวข้องกับสตริงทั้งหมด

ปัจจัยทางภาษาบางอย่างสามารถทำให้แย่ลงได้ ใน C ตัวอย่างเช่นสตริงเป็นเพียงตัวชี้ไปยังอาร์เรย์ของอักขระที่สิ้นสุดด้วยค่า null ซึ่งหมายความว่าคุณไม่รู้ว่ามันอยู่นานแค่ไหนดังนั้นจึงไม่มีวิธีที่จะปรับห่วงการคัดลอกสตริงด้วยการดำเนินการย้ายอย่างรวดเร็ว คุณต้องคัดลอกทีละตัวเพื่อให้คุณสามารถทดสอบแต่ละไบต์สำหรับ null terminator


4
และบางภาษาทำให้ดีขึ้นมาก: การเข้ารหัสของความยาวสตริงของ Delphi ที่จุดเริ่มต้นของอาร์เรย์ทำให้การต่อสตริงเร็วมาก
Frank Shearar

4
@gablin: นอกจากนี้ยังช่วยให้การคัดลอกสตริงเร็วขึ้นด้วย เมื่อคุณรู้ขนาดล่วงหน้าคุณไม่จำเป็นต้องคัดลอกทีละหนึ่งไบต์และตรวจสอบแต่ละไบต์เพื่อหาค่า null terminator ดังนั้นคุณสามารถใช้ขนาดเต็มของการลงทะเบียนใด ๆ รวมถึง SIMD สำหรับการเคลื่อนย้ายข้อมูล มันเร็วกว่าถึง 16 เท่า
Mason Wheeler

4
@ มาธิปิก: ใช่และมันก็ดีสำหรับมันเท่าที่จะพาคุณ แต่เมื่อคุณเริ่มต้นการโต้ตอบกับ libc หรือรหัสภายนอกอื่น ๆ มันคาดว่า a char*, ไม่ใช่strbuf, และคุณกลับไปที่ตาราง 1 มีเพียงคุณเท่านั้น สามารถทำเมื่อการออกแบบที่ไม่ดีถูกอบเป็นภาษา
Mason Wheeler

6
@ คณิตศาสตร์: แน่นอนมีbufตัวชี้อยู่ที่นั่น ฉันไม่เคยตั้งใจจะบอกเป็นนัยว่ามันไม่สามารถใช้ได้ ค่อนข้างว่ามันจำเป็น โค้ดใด ๆ ที่ไม่ทราบเกี่ยวกับประเภทสตริงที่ได้รับการปรับปรุง แต่ไม่ได้มาตรฐานรวมถึงสิ่งที่เป็นพื้นฐานของไลบรารี่มาตรฐานยังคงต้องถอยกลับช้าและไม่ปลอดภัย char*คุณสามารถเรียก FUD นั้นได้ถ้าคุณต้องการ แต่นั่นก็ไม่ได้ทำให้มันไม่เป็นความจริง
Mason Wheeler

7
ผู้คนมีคอลัมน์ Joel Spolsky เกี่ยวกับประเด็นของ Frank Shearer's: กลับไปสู่พื้นฐาน
user16764

14

นี่เป็นหัวข้อเก่าและฉันคิดว่าคำตอบอื่น ๆ นั้นยอดเยี่ยม แต่มองข้ามบางสิ่งดังนั้นนี่คือ 2 เซ็นต์ของฉัน

การเคลือบน้ำตาลซินแทคติกซ่อนความซับซ้อน

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

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

รายละเอียดการใช้งาน

Good Ol 'Array

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

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

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

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

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

สนับสนุน Unicode

นอกจากนี้และอีกครั้งนี้เกิดจากความจริงที่ว่าการเคลือบน้ำตาล syntactic ของภาษาของคุณซ่อนไว้จากคุณที่จะเล่นที่ดีคุณมักจะไม่คิดว่ามันเป็นเงื่อนไขของการสนับสนุน Unicode (โดยเฉพาะตราบใดที่คุณไม่ต้องการมันจริงๆ และชนกำแพงนั้น) และบางภาษากำลังคิดไปข้างหน้าอย่าใช้สายอักขระกับอาเรย์พื้นฐาน 8 บิตแบบดั้งเดิม พวกเขาอบใน UTF-8 หรือ UTF-16 หรือสิ่งที่คุณมีให้การสนับสนุนและผลที่ตามมาคือการใช้หน่วยความจำขนาดใหญ่มากซึ่งมักจะไม่จำเป็นต้องใช้เวลาและเวลาประมวลผลขนาดใหญ่เพื่อจัดสรรหน่วยความจำประมวลผลสตริง และใช้ตรรกะทั้งหมดที่เกิดขึ้นควบคู่กับการจัดการจุดรหัส


ผลลัพธ์ของสิ่งนี้คือเมื่อคุณทำสิ่งที่เทียบเท่าในรหัสหลอกไปที่:

hello = "hello,"
world = " world!"
str = hello + world

มันอาจจะไม่ใช่ - แม้จะมีความพยายามอย่างเต็มที่ที่นักพัฒนาภาษาจะต้องทำตามที่คุณต้องการยกเว้น - เป็นเรื่องง่าย:

a = 1;
b = 2;
shouldBeThree = a + b

ในการติดตามคุณอาจต้องการอ่าน:


นอกจากนี้ยังมีการอภิปรายที่ดี
Abel

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

@bel: ขอบคุณดูเหมือนว่าฉันเป็นห้องสำหรับรายละเอียดเพิ่มเติมทั่วไป
haylem

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

1

วลีที่ว่า "การดำเนินงานค่าเฉลี่ย" น่าจะเป็นชวเลขสำหรับการทำงานครั้งเดียวของทฤษฎีสุ่มการเข้าถึงที่จัดเก็บ-โปรแกรมเครื่อง นี่เป็นเครื่องจักรตามทฤษฎีที่ใช้ในการวิเคราะห์เวลาทำงานของอัลกอริทึมต่างๆ

โดยทั่วไปการดำเนินการทั่วไปจะถูกดำเนินการเพื่อโหลดเพิ่มลบจัดเก็บสาขา อาจจะอ่านพิมพ์และหยุด

แต่การดำเนินการสตริงส่วนใหญ่ต้องการการดำเนินงานพื้นฐานหลายอย่าง ตัวอย่างเช่นการทำสำเนาสตริงโดยปกติต้องใช้การคัดลอกและด้วยเหตุนี้จำนวนของการดำเนินการซึ่งเป็นสัดส่วนกับความยาวของสตริง (นั่นคือมัน "เชิงเส้น") การค้นหาสตริงย่อยภายในสตริงอื่นยังมีความซับซ้อนเชิงเส้น


1

มันขึ้นอยู่กับการดำเนินการทั้งหมดวิธีการแสดงสตริงและการเพิ่มประสิทธิภาพที่มีอยู่ หากสตริงมีความยาว 4 หรือ 8 ไบต์ (และจัดตำแหน่ง) พวกเขาจะไม่จำเป็นต้องช้าลง - การดำเนินการหลายอย่างจะเร็วพอ ๆ กัน หรือถ้าสตริงทั้งหมดมีแฮชแบบ 32- บิตหรือ 64- บิตการดำเนินการจำนวนมากก็จะเร็วเหมือนกัน (แม้ว่าคุณจะจ่ายค่าแฮชราคาล่วงหน้า)

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


0

ให้ฉันตอบคำถามของคุณด้วยคำถาม เหตุใดการพูดชุดคำศัพท์จึงใช้เวลานานกว่าคำเดียว


2
มันไม่จำเป็นเลย
user16764

3
Supercalifragilisticexpialidocious
Spoike

s / คำ / พยางค์ / g
Caleb

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