ค้นหาเว็บไซต์

การสร้าง PowerShell เพื่อความเร็ว


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

เราจะไม่ครอบคลุมสคริปต์ PowerShell แบบมัลติเธรดในบทความนี้ คุณสามารถอ่านทั้งหมดเกี่ยวกับเรื่องนั้นได้ในบทความ PowerShell Multithreading: A Deep Dive ในบทความนี้ เราจะมุ่งเน้นไปที่การวัดประสิทธิภาพ การเร่งความเร็วลูป การใช้อาร์เรย์เพื่อความเร็ว และอื่นๆ อีกมากมาย!

การวัดความเร็วของ PowerShell

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

ในกรณีส่วนใหญ่ คุณสามารถเรียกใช้ Measure-Command โดยใช้โค้ดของคุณภายใน scriptblock พารามิเตอร์ Expression และรับสิ่งที่คุณกำลังมองหา แต่ในกรณีที่โค้ดของคุณอาจทำงานได้อย่างรวดเร็วอยู่แล้ว และคุณวัดความแตกต่างได้ในสิบมิลลิวินาทีหรือน้อยกว่า คุณอาจต้องวนโค้ดของคุณเหมือนกับโค้ดด้านล่าง

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

foreach ($i in 1..1000) {
	Get-ChildItem
}

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

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

New-Item .\test.txt
Measure-Command {Get-FileHash .\test.txt}
Measure-Command {Get-FileHash .\test.txt}
Measure-Command {Get-FileHash .\test.txt}

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

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

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

Foreach-Object กับ Foreach ความเร็วของลูป

แม้ว่าลูปมักจะถูกใช้เพื่อประมวลผลผ่านหลายรายการ แต่ก็อาจเป็นตัวปัญหาเมื่อเกิดปัญหาด้านประสิทธิภาพ

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

1..10000 | ForEach-Object {
	if ($Last) { $Last * $_ } 
	$Last = $_
}

และนี่คือผลลัพธ์จากโค้ดด้านบน:

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

foreach ($Num in 1..10000) { 
	if ($Last) { $Last * $Num } 
	$Last = $Num
}

โค้ดนี้ให้เอาต์พุตเดียวกัน แต่เกิดจากการใช้คำสั่ง foreach แทน ForEach-Object

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

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

เมื่อรันคำสั่ง Get-ChildItem คำสั่งจะแสดงข้อมูลเกี่ยวกับไฟล์ รวมถึงเส้นทางไปยังไฟล์ด้วย ซึ่งสามารถใช้ได้โดยตรงผ่านไปป์ไลน์ด้วยคำสั่ง Get-FileHash

foreach ($File in Get-ChildItem) { 
	Get-FileHash $File
}

วิธีที่ดีกว่าในการเขียนโค้ดนี้คือไปป์ไลน์เดียวโดยไม่มีการวนซ้ำดังนี้:

Get-ChildItem | Get-FileHash

ชุดโค้ดเหล่านี้จะแสดงผลเหมือนกัน แต่ชุดหลังแสดงประสิทธิภาพที่เพิ่มขึ้นอย่างมาก

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

การจัดการลูปใน PowerShell เพื่อความรวดเร็วจะช่วยเพิ่มประสิทธิภาพได้อย่างมาก

อาร์เรย์เทียบกับรายการ: PowerShell Speed++

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

$Array = @()
foreach ($Num in 1..10000) {
	if ($Last) { $Array += $Last * $Num } 
	$Last = $Num
}

ในตัวอย่างนี้ ผลลัพธ์จะถูกบันทึกลงใน $Array เพื่อใช้ในภายหลังหรือเพื่อส่งออกผลลัพธ์ทั้งหมด

แทนที่จะใช้อาร์เรย์ คุณควรใช้วัตถุ ArrayList ไวยากรณ์ที่ใช้แตกต่างกันเล็กน้อย ขั้นแรก คุณต้องกำหนด ArrayList โดยเฉพาะด้วย New-Object System.Collections.ArrayList นอกจากนี้ คุณจะต้องใช้เมธอด .Add() แทน += เหมือนที่ทำกับอาร์เรย์ นี่คือบล็อกโค้ดที่อัปเดตเพื่อใช้ ArrayList แทนอาร์เรย์ปกติ:

$ArrayList = New-Object System.Collections.ArrayList
foreach ($Num in 1..10000) { 
	if ($Last) { $ArrayList.Add($Last * $Num) }
	$Last = $Num
}

นี่คือผลลัพธ์จากโค้ดด้านบน

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

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

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

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

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

กรองไปทางซ้าย

คำสั่งทั่วไปอย่างหนึ่งใน PowerShell คือ Where-Object ซึ่งใช้สำหรับการกรองเอาต์พุตของคำสั่งอื่น แม้ว่าจะมีพลังมาก แต่ก็ควรใช้เฉพาะเมื่อจำเป็นเท่านั้น ตรวจสอบให้แน่ใจว่าคุณ "กรองไปทางซ้าย" ในตำแหน่งที่เป็นไปได้เพื่อเพิ่มความเร็วของสคริปต์ PowerShell

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

ตัวอย่างเช่น นี่คือบล็อกโค้ดเพื่อรับรายการในโฟลเดอร์ จากนั้นกรองไปยังรายการที่ขึ้นต้นด้วยหมายเลข 1

Get-ChildItem | Where-Object {$_.Name -like '1*'}

โค้ดด้านบนเสร็จสิ้นอย่างรวดเร็วในเวลาเพียง 422ms แต่ก็ยังดีกว่านี้ได้ แทนที่จะกรองหลังจากรันคำสั่งแล้ว คุณสามารถกรองภายในคำสั่งได้โดยใช้คำสั่งด้านล่างแทน:

Get-ChildItem -Filter 1*

คุณจะได้รับเอาต์พุตเดียวกัน แต่จะลดปริมาณข้อมูลที่จัดการผ่านไปป์ไลน์ ดังนั้นจึงเสร็จสิ้นภายใน 30 มิลลิวินาที ความแตกต่างขนาดใหญ่นี้เกิดจากวิธีการทำงานของตัวกรอง ในตัวอย่างแรก ทุกไฟล์ไดเร็กทอรีถูกส่งไปยังคำสั่ง Where-Object เพื่อดูว่าควรดำเนินการต่อหรือไม่ ในตัวอย่างที่สอง Get-ChildItem จะกรองผลลัพธ์ก่อนผลลัพธ์ใดๆ ที่ถูกส่งไปตามไปป์ไลน์

เริ่มต้นด้วย PowerShell 3 คุณสามารถใช้ Where-Object ได้โดยไม่ต้องมีบล็อกสคริปต์ ขึ้นอยู่กับบล็อกสคริปต์ที่คุณใช้ อาจเพิ่มความเร็วในการดำเนินการได้เล็กน้อย ในโค้ดด้านล่างนี้ จะบันทึกได้เฉลี่ยเพียง 10ms เท่านั้น กลับไปที่ตัวอย่างดั้งเดิม รูปแบบบล็อกที่ไม่ใช่สคริปต์จะมีลักษณะดังนี้:

Get-ChildItem | Where-Object Name -like '1*'

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

(Get-ChildItem).Where({$_.Name -like '1*'})

ตัวเลือกนี้ดำเนินการได้เร็วกว่าตัวเลือกอื่นๆ ที่กล่าวถึงข้างต้นมาก ความแตกต่างของประสิทธิภาพนี้แสดงให้เห็นว่าในกรณีที่ cmdlet ที่คุณใช้ไม่รองรับพารามิเตอร์ตัวกรอง และคุณไม่จำเป็นต้องรองรับ PowerShell เวอร์ชันเก่าที่ใช้วิธี .Where() น่าจะเป็น ตัวเลือกที่ดีที่สุดสำหรับความเร็ว

ดังที่กล่าวไปแล้ว วิธีการ .Where() อาจอ่านยากขึ้นเล็กน้อย ดังนั้นคุณอาจเลือกใช้รูปแบบ Where-Object {} ที่เป็นมาตรฐานมากขึ้นเพื่อให้อ่านง่ายขึ้น ทั้งนี้ขึ้นอยู่กับ มันสร้างความแตกต่างได้มากเพียงใดในกรณีของคุณ

เกิดข้อผิดพลาด การตรวจสอบก่อนดำเนินการ

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

ตัวอย่างเช่น คุณต้องการลบไฟล์หากมีอยู่ และไม่ต้องดำเนินการใดๆ หากไม่มีอยู่ คุณสามารถใช้โค้ดด้านล่างได้ มันจะวนซ้ำไฟล์ทั้งหมดในไดเรกทอรีปัจจุบันที่มีป้ายกำกับ 1 ถึง 10,000 และลบออกหากมีอยู่

foreach ($Item in 1..10000) {Remove-Item $Item}

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

foreach ($Item in 1..10000) {Remove-Item $Item -ErrorAction SilentlyContinue}

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

แทนที่จะสุ่มสี่สุ่มห้าพยายามลบไฟล์ที่มีอยู่หรือไม่ คุณสามารถตรวจสอบได้ว่าไฟล์นั้นมีอยู่ก่อนหรือไม่ และลบออกเฉพาะในกรณีที่ไฟล์นั้นมีโค้ดนี้:

foreach ($Item in 1..10000) {
	if (Test-Path $Item) { Remove-Item $Item }
}

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

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

สรุป

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

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

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

การอ่านเพิ่มเติม

หากต้องการเจาะลึกถึงความแตกต่างระหว่าง Arrays และ ArrayLists คุณสามารถดูบทความนี้ได้ หากต้องการความช่วยเหลือในการทำโค้ด PowerShell แบบมัลติเธรด คุณสามารถดูได้ที่นี่

บทความที่เกี่ยวข้อง