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


63

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

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

ถ้าฉันเขียนโปรแกรมเดียวกันใน Scala (ซึ่งใช้ JVM) การดำเนินการนี้จะเร็วขึ้นหรือไม่ ถ้าใช่ทำไม มันเป็นเพราะสไตล์การเขียนหรือไม่? ถ้าเป็นเพราะสไตล์การเขียนตอนนี้ที่ Java รวมแลมบ์ดานิพจน์แล้วฉันไม่สามารถบรรลุผลลัพธ์เดียวกันโดยใช้ Java กับแลมบ์ดาได้หรือไม่? หรือเร็วกว่านี้เพราะสกาล่าจะรวบรวมสิ่งต่าง ๆ ?


64
การเขียนโปรแกรมการทำงานของ Afaik ไม่ได้ทำให้การมัลติเธรดเร็วขึ้น มันทำให้การใช้งานแบบมัลติเธรดง่ายขึ้นและปลอดภัยยิ่งขึ้นเนื่องจากมีคุณสมบัติบางอย่างของฟังก์ชั่นการเขียนโปรแกรมเช่นการใช้งานไม่ได้และฟังก์ชั่นที่ไม่มีผลข้างเคียงซึ่งช่วยในเรื่องนี้
Pieter B

7
โปรดทราบว่า 1) ดีกว่าที่ไม่ได้กำหนดไว้จริงๆ 2) มันเป็นที่แน่นอน ไม่ได้กำหนดให้เป็นเพียงแค่ "เร็วขึ้น" ภาษา X ที่ต้องใช้รหัสพันล้านเท่าของขนาดสำหรับการได้รับประสิทธิภาพ 0.1% ที่เกี่ยวข้องกับ Y นั้นไม่ดีกว่า Y สำหรับคำจำกัดความที่สมเหตุสมผลใด ๆ ที่ดีกว่า
Bakuriu

2
คุณหมายถึงถามเกี่ยวกับ "ฟังก์ชั่นการเขียนโปรแกรม" หรือ "โปรแกรมที่เขียนในลักษณะการทำงาน" หรือไม่? บ่อยครั้งที่การเขียนโปรแกรมที่เร็วกว่าไม่ทำให้โปรแกรมเร็วขึ้น
Ben Voigt

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

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

คำตอบ:


97

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

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

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

TL; DR: หากโซลูชันใน Scala มีประสิทธิภาพมากขึ้นในการประมวลผลแบบขนานมากกว่าหนึ่งใน Java นั่นไม่ใช่เพราะวิธีการรวบรวมหรือเรียกใช้รหัสผ่าน JVM แต่เนื่องจากโซลูชัน Java กำลังแบ่งปันสถานะที่ไม่แน่นอนระหว่างเธรด อาจทำให้เกิดสภาพการแข่งขันหรือการเพิ่มโอเวอร์เฮดของการซิงโครไนซ์เพื่อหลีกเลี่ยง


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

31
การมีเธรดหนึ่งกลายพันธุ์ข้อมูลในขณะที่คนอื่นอ่านก็เพียงพอแล้วที่คุณจะต้องเริ่มต้น "การดูแลเป็นพิเศษ"
Peter Green

10
@Brendan: ไม่หากเธรดหนึ่งแก้ไขข้อมูลในขณะที่เธรดอื่นกำลังอ่านจากข้อมูลเดียวกันนั่นแสดงว่าคุณมีสภาวะการแย่งชิง จำเป็นต้องมีการดูแลเป็นพิเศษแม้ว่าจะมีการแก้ไขเพียงเธรดเดียวเท่านั้น
Cornstalks

3
สถานะที่ไม่แน่นอนคือ "รากเหง้าแห่งความชั่วร้าย" ในบริบทของการประมวลผลแบบขนาน => หากคุณยังไม่ได้ดูสนิมฉันขอแนะนำให้คุณดูมัน มันจัดการเพื่อให้เกิดความไม่แน่นอนได้อย่างมีประสิทธิภาพมากโดยตระหนักว่าปัญหาที่แท้จริงนั้นไม่แน่นอนกับการผสม: ถ้าคุณมีนามแฝงหรือมีความไม่แน่นอนเท่านั้นก็ไม่มีปัญหา
Matthieu M.

2
@MatthieuM ถูกต้องขอบคุณ! ฉันแก้ไขเพื่อแสดงสิ่งต่าง ๆ อย่างชัดเจนยิ่งขึ้นในคำตอบของฉัน สถานะที่เปลี่ยนแปลงไม่ได้เป็นเพียง "รากเหง้าของความชั่วทั้งหมด" เมื่อมีการแบ่งปันระหว่างกระบวนการที่เกิดขึ้นพร้อมกันซึ่งเป็นสิ่งที่สนิมจะหลีกเลี่ยงด้วยกลไกการควบคุมความเป็นเจ้าของ
MichelHenrich

8

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

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


7

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

  1. การหลีกเลี่ยงสถานะที่ไม่แน่นอนมีแนวโน้มที่จะลดจำนวนของสิ่งต่าง ๆ ที่อาจผิดพลาดในโปรแกรมและยิ่งไปกว่านั้นในโปรแกรมที่เกิดขึ้นพร้อมกัน
  2. การหลีกเลี่ยงการใช้หน่วยความจำร่วมและการซิงโครไนซ์แบบล็อกตามแนวคิดระดับสูงมักทำให้การซิงโครไนซ์ระหว่างเธรดของรหัสง่ายขึ้น

หนึ่งในตัวอย่างที่ดีของจุดที่ 2 คือว่าใน Haskell เรามีความแตกต่างที่ชัดเจนระหว่างความเท่าเทียมกำหนดขึ้นเมื่อเทียบกับการเห็นพ้องด้วยที่ไม่ได้กำหนด ไม่มีคำอธิบายใดที่ดีไปกว่าการอ้างถึงหนังสือที่ยอดเยี่ยมของ Simon Marlow การเขียนโปรแกรมแบบขนานและแบบพร้อมกันใน Haskell (คำพูดมาจากบทที่ 1 ):

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

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

นอกเหนือจากนี้มาร์โลว์ยังกล่าวถึงมิติของการกำหนด :

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

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

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

  • กำหนดคุณสมบัติและห้องสมุดขนาน
  • แบบไม่กำหนดคุณสมบัติและห้องสมุดเห็นพ้องด้วย

หากคุณเพียงแค่พยายามเพิ่มความเร็วในการคำนวณบริสุทธิ์ที่กำหนดขึ้นการมีความเท่าเทียมแบบกำหนดแน่นอนมักทำให้สิ่งต่าง ๆ ง่ายขึ้น บ่อยครั้งที่คุณทำอะไรแบบนี้:

  1. เขียนฟังก์ชั่นที่สร้างรายการคำตอบซึ่งแต่ละอันมีราคาแพงในการคำนวณ แต่ไม่ได้ขึ้นอยู่กับกันและกัน นี่คือ Haskell ดังนั้นรายการจะขี้เกียจ - คุณค่าขององค์ประกอบของพวกเขาไม่ได้คำนวณจริงจนกว่าผู้บริโภคต้องการพวกเขา
  2. ใช้ไลบรารีStrategiesเพื่อใช้อิลิเมนต์รายการผลลัพธ์ของฟังก์ชันของคุณควบคู่ไปกับหลายคอร์

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

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


2

ใน Haskell การปรับเปลี่ยนนั้นเป็นไปไม่ได้อย่างแท้จริงโดยไม่ได้รับตัวแปรที่แก้ไขได้พิเศษผ่านไลบรารีการปรับเปลี่ยน แต่ฟังก์ชั่นสร้างตัวแปรที่พวกเขาต้องการในเวลาเดียวกันกับค่าของพวกเขา (ซึ่งคำนวณอย่างขี้เกียจ) และเก็บขยะเมื่อไม่ต้องการอีกต่อไป

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

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

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

การอ่าน
แบบขนานใน Haskell (HaskellWiki)
การเขียนโปรแกรมพร้อมกันและมัลติคอร์ในโปรแกรม "Real-World Haskell"
การเขียนโปรแกรมแบบขนานและพร้อมกันใน Haskell โดย Simon Marlow


7
grep java this_post. grep scala this_postและgrep jvm this_postไม่ส่งคืนผลลัพธ์ :)
Andres F.

4
คำถามที่คลุมเครือ ในชื่อและวรรคแรกจะถามเกี่ยวกับการเขียนโปรแกรมการทำงานโดยทั่วไปในวรรคที่สองและสามจะถามเกี่ยวกับชวาและสกาล่าโดยเฉพาะอย่างยิ่ง นั่นเป็นเรื่องที่โชคร้ายโดยเฉพาะอย่างยิ่งเมื่อหนึ่งในจุดแข็งหลักของสกาล่าคือความจริงที่ว่าไม่ใช่ภาษาที่ใช้งานได้ Martin Odersky เรียกมันว่า "post-functional" ส่วนคนอื่นเรียกมันว่า "object-functional" มีคำจำกัดความสองคำที่แตกต่างกันของคำว่า หนึ่งคือ "การเขียนโปรแกรมด้วยขั้นตอนชั้นหนึ่ง" (คำจำกัดความดั้งเดิมตามที่ใช้กับ LISP) อีกรายการคือ ...
Jörg W Mittag

2
"การเขียนโปรแกรมด้วยฟังก์ชั่นที่โปร่งใสอ้างอิงบริสุทธิ์ผลข้างเคียงและข้อมูลถาวรที่ไม่เปลี่ยนรูป" (มีความเข้มงวดมากขึ้นและการตีความที่ใหม่กว่า) คำตอบนี้กล่าวถึงการตีความครั้งที่สองซึ่งสมเหตุสมผลเนื่องจากก) การตีความครั้งแรกนั้นไม่เกี่ยวข้องกับการขนานและการเกิดขึ้นพร้อมกันโดยสิ้นเชิงข) การตีความครั้งแรกได้กลายเป็นความหมายโดยทั่วไปเนื่องจากไม่มีข้อยกเว้นของภาษาเกือบทุกภาษา วันนี้มีขั้นตอนแรก (รวมถึง Java) และ c) OP ถามเกี่ยวกับความแตกต่างระหว่าง Java และ Scala แต่ไม่มี ...
Jörg W Mittag

2
ระหว่างทั้งสองเกี่ยวกับคำจำกัดความ # 1 นิยามเพียง # 2
Jörg W Mittag

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