2010-07-28 28 views
13

Thật ngạc nhiên khi sử dụng PLINQ không mang lại lợi ích cho một trường hợp thử nghiệm nhỏ mà tôi đã tạo; trên thực tế, nó thậm chí còn tồi tệ hơn LINQ thông thường.PLINQ Thực hiện tồi tệ hơn thông thường LINQ

Dưới đây là các mã kiểm tra:

int repeatedCount = 10000000; 
    private void button1_Click(object sender, EventArgs e) 
    { 
     var currTime = DateTime.Now; 
     var strList = Enumerable.Repeat(10, repeatedCount); 
     var result = strList.AsParallel().Sum(); 

     var currTime2 = DateTime.Now; 
     textBox1.Text = (currTime2.Ticks-currTime.Ticks).ToString(); 

    } 

    private void button2_Click(object sender, EventArgs e) 
    { 
     var currTime = DateTime.Now; 
     var strList = Enumerable.Repeat(10, repeatedCount); 
     var result = strList.Sum(); 

     var currTime2 = DateTime.Now; 
     textBox2.Text = (currTime2.Ticks - currTime.Ticks).ToString(); 
    } 

Kết quả?

textbox1: 3437500 
textbox2: 781250 

Vì vậy, LINQ mất ít thời gian hơn PLINQ để hoàn tất một hoạt động tương tự!

Tôi đang làm gì sai? Hoặc là có một twist mà tôi không biết về?

Chỉnh sửa: Tôi đã cập nhật mã của mình để sử dụng đồng hồ bấm giờ, tuy nhiên, hành vi tương tự vẫn tồn tại. Để giảm hiệu quả của JIT, tôi thực sự đã thử một vài lần bằng cách nhấp vào cả hai button1button2 và không theo thứ tự cụ thể. Mặc dù thời gian tôi nhận được có thể khác, nhưng hành vi định tính vẫn còn: PLINQ thực sự chậm hơn trong trường hợp này.

+4

Mẹo: Sử dụng lớp 'Đồng hồ bấm giờ' để đo hiệu suất. Sẽ chính xác hơn khi đo thời gian hơn 'DateTime.Now'. – Greg

+2

@Anthony Pegram, như thế nào? Tôi có thể dễ dàng thấy mọi loại sẽ như thế nào. – CaffGeek

+0

Bạn có thể cho chúng tôi biết bạn đang chạy phần cứng nào không? –

Trả lời

21

Đầu tiên: Dừng sử dụng DateTime để đo thời gian chạy. Thay vào đó, hãy sử dụng Đồng hồ bấm giờ. Các mã kiểm tra sẽ trông như thế:

var watch = new Stopwatch(); 

var strList = Enumerable.Repeat(10, 10000000); 

watch.Start(); 
var result = strList.Sum(); 
watch.Stop(); 

Console.WriteLine("Linear: {0}", watch.ElapsedMilliseconds); 

watch.Reset(); 

watch.Start(); 
var parallelResult = strList.AsParallel().Sum(); 
watch.Stop(); 

Console.WriteLine("Parallel: {0}", watch.ElapsedMilliseconds); 

Console.ReadKey(); 

Thứ hai: Chạy điều trong Parallel thêm overhead. Trong trường hợp này, PLINQ phải tìm ra cách tốt nhất để chia bộ sưu tập của bạn để nó có thể tổng hợp các phần tử một cách an toàn song song. Sau đó, bạn cần phải tham gia các kết quả từ các chủ đề khác nhau tạo ra và Sum những người là tốt. Đây không phải là một nhiệm vụ tầm thường.

Sử dụng mã ở trên tôi có thể thấy rằng sử dụng Sum() lưới một cuộc gọi ~ 95ms. Gọi .AsParallel(). Sum() lưới xung quanh ~ 185ms.

Thực hiện tác vụ song song chỉ là ý tưởng hay nếu bạn đạt được điều gì đó bằng cách thực hiện. Trong trường hợp này, Sum là một nhiệm vụ đủ đơn giản mà bạn không đạt được bằng cách sử dụng PLINQ.

+1

+1 @Justin Niessner Rất Crisp câu trả lời. @Ngu Soon Hui PLINQ sẽ rất tiện dụng, nhưng nếu bạn không hiểu rõ nó cũng sẽ là ác. Tôi sẽ đề nghị bạn xem Cache False Sharing và GC Throttling trước khi bắt đầu chơi với PLINQ. –

+0

Rõ ràng là không có khả năng của hệ thống để nhận ra thực tế rằng việc sử dụng PLINQ là mâu thuẫn chậm hơn với tuyên bố trong bài đăng này http://stackoverflow.com/questions/2893637/is-it-ok-to-try-to-use-plinq- trong tất cả các truy vấn-linq mà hệ thống có thể chọn LINQ hoặc PLINQ tùy thuộc vào cái nào nhanh hơn. – Schultz9999

+0

chạy mã trong đơn, plinq nhanh hơn một chút – CuiPengFei

1

Có thể bạn không tính đến thời gian JIT không? Bạn nên chạy thử nghiệm của mình hai lần và loại bỏ tập hợp kết quả đầu tiên.

Ngoài ra, bạn không nên sử dụng DateTime để có được thời gian thực hiện, sử dụng lớp Stopwatch thay vì:

var swatch = new Stopwatch(); 
swatch.StartNew(); 

var strList = Enumerable.Repeat(10, repeatedCount); 
var result = strList.AsParallel().Sum(); 

swatch.Stop(); 
textBox1.Text = swatch.Elapsed; 

PLINQ không thêm một số nguyên cần thiết để xử lý một chuỗi. Nhưng sự khác biệt trong trường hợp của bạn có vẻ quá mức. PLINQ có ý nghĩa khi chi phí trên cao hơn nhiều so với lợi ích của việc chạy logic trên nhiều lõi/CPU. Nếu bạn không có nhiều lõi, việc chạy xử lý song song cung cấp không có lợi thế thực - và PLINQ sẽ phát hiện một trường hợp như vậy và thực hiện xử lý tuần tự.

EDIT: Khi tạo thử nghiệm hiệu suất nhúng loại này, bạn nên đảm bảo rằng bạn không chạy chúng dưới trình gỡ lỗi hoặc đã bật Intellitrace, vì chúng có thể làm giảm đáng kể thời gian hoạt động.

0

Điều đó thực sự có thể là trường hợp vì bạn đang tăng số lượng thiết bị chuyển mạch ngữ cảnh và bạn không thực hiện bất kỳ hành động nào có lợi cho việc có chuỗi chờ hoàn thành. Điều này sẽ tồi tệ hơn nếu bạn đang chạy trong một hộp cpu duy nhất.

0

Tôi khuyên bạn nên sử dụng lớp Đồng hồ bấm giờ cho các chỉ số thời gian. Trong trường hợp của bạn, nó là thước đo tốt hơn của khoảng thời gian.

0

Vui lòng đọc phần Tác dụng phụ của bài viết này.

http://msdn.microsoft.com/en-us/magazine/cc163329.aspx

Tôi nghĩ bạn có thể chạy vào nhiều điều kiện nơi PLINQ có mô hình xử lý dữ liệu bổ sung mà bạn phải hiểu trước khi bạn chọn để nghĩ rằng là sẽ luôn luôn hoàn toàn có thời gian đáp ứng nhanh hơn.

+0

Không có bất kỳ tác dụng phụ ở đây mặc dù ... –

8

Một số khác đã chỉ ra một số sai sót trong điểm chuẩn của bạn. Dưới đây là một ứng dụng console ngắn để làm cho nó đơn giản hơn:

using System; 
using System.Diagnostics; 
using System.Linq; 

public class Test 
{ 
    const int Iterations = 1000000000; 

    static void Main() 
    { 
     // Make sure everything's JITted 
     Time(Sequential, 1); 
     Time(Parallel, 1); 
     Time(Parallel2, 1); 
     // Now run the real tests 
     Time(Sequential, Iterations); 
     Time(Parallel, Iterations); 
     Time(Parallel2, Iterations); 
    } 

    static void Time(Func<int, int> action, int count) 
    { 
     GC.Collect(); 
     Stopwatch sw = Stopwatch.StartNew(); 
     int check = action(count); 
     if (count != check) 
     { 
      Console.WriteLine("Check for {0} failed!", action.Method.Name); 
     } 
     sw.Stop(); 
     Console.WriteLine("Time for {0} with count={1}: {2}ms", 
          action.Method.Name, count, 
          (long) sw.ElapsedMilliseconds); 
    } 

    static int Sequential(int count) 
    { 
     var strList = Enumerable.Repeat(1, count); 
     return strList.Sum(); 
    } 

    static int Parallel(int count) 
    { 
     var strList = Enumerable.Repeat(1, count); 
     return strList.AsParallel().Sum(); 
    } 

    static int Parallel2(int count) 
    { 
     var strList = ParallelEnumerable.Repeat(1, count); 
     return strList.Sum(); 
    } 
} 

Compilation:

csc /o+ /debug- Test.cs 

Kết quả trên máy tính xách tay quad core i7 của tôi; chạy nhanh tới 2 lõi, hoặc 4 lõi chậm hơn. Về cơ bản, chiến thắng ParallelEnumerable.Repeat, tiếp theo là phiên bản chuỗi, tiếp theo là song song với thông thường Enumerable.Repeat.

Time for Sequential with count=1: 117ms 
Time for Parallel with count=1: 181ms 
Time for Parallel2 with count=1: 12ms 
Time for Sequential with count=1000000000: 9152ms 
Time for Parallel with count=1000000000: 44144ms 
Time for Parallel2 with count=1000000000: 3154ms 

Lưu ý rằng các phiên bản trước của câu trả lời này rất đáng tiếc vì có sai số thành phần - Tôi tự tin hơn nhiều trong kết quả ở trên.

+0

Tôi không thấy bất kỳ sự khác biệt cơ bản giữa điểm chuẩn của bạn và của tôi, nhưng tại sao kết quả của chúng tôi là khác nhau đáng kể? – Graviton

+1

@Ngu: Bạn có đang chạy mã của mình dưới trình gỡ lỗi hoặc sử dụng tính năng Intellitrace của VS2010 không? Chúng có thể làm xáo trộn đáng kể kết quả cho thời gian thực hiện. Lý tưởng nhất, hãy thử chạy bản phát hành xây dựng trên dòng lệnh - không phải từ VS. – LBushkin

+1

@Ngu: 1) Của tôi không chạy trong bối cảnh giao diện người dùng. Tôi không biết liệu điều đó có tạo nên sự khác biệt nào không, nhưng tại sao lại cung cấp thêm điểm chuẩn cho công việc? 2) Mỏ rõ ràng là loại bỏ JIT khỏi phương trình. 3) Mỏ mang lại cho công việc nhiều việc phải làm hơn (theo hệ số 100). Sự khác biệt Đồng hồ bấm giờ/Ngày giờ là một * bit * của một cá trích đỏ, IMO - vào thời điểm bạn có các khoảng thời gian dài hợp lý liên quan (nhiều giây) sự thiếu chính xác của DateTime sẽ không đáng kể. –

0

Nhận xét của Justin về chi phí là chính xác.

Chỉ cần một cái gì đó để xem xét khi viết phần mềm đồng thời nói chung, ngoài việc sử dụng các PLINQ:

Bạn luôn cần phải được suy nghĩ về "granularity" của hạng mục công trình của bạn. Một số vấn đề rất thích hợp để song song vì chúng có thể được "chunked" ở mức rất cao, giống như raytracing toàn bộ khung hình đồng thời (những loại vấn đề này được gọi là lúng túng song song). Khi có rất nhiều "khối" công việc, thì chi phí tạo và quản lý nhiều luồng sẽ trở nên không đáng kể so với công việc thực tế mà bạn muốn hoàn thành.

PLINQ giúp lập trình đồng thời dễ dàng hơn, nhưng điều đó không có nghĩa là bạn có thể bỏ qua suy nghĩ về mức độ chi tiết của công việc của bạn.

22

Đây là một sai lầm kinh điển - suy nghĩ, "Tôi sẽ chạy một thử nghiệm đơn giản để so sánh hiệu suất của mã đơn luồng này với mã đa luồng này."

Một đơn giản thử nghiệm là tồi tệ nhất loại xét nghiệm bạn có thể chạy để đo hiệu suất đa luồng.

Thông thường, song song một số thao tác sẽ mang lại lợi ích hiệu suất khi các bước bạn đang song song yêu cầu công việc đáng kể. Khi các bước là đơn giản - như trong, nhanh chóng * - chi phí của việc song song công việc của bạn kết thúc lên dwarfing đạt được hiệu suất miniscule bạn sẽ có nếu không nhận được.


Hãy xem xét sự tương tự này.

Bạn đang xây dựng một tòa nhà.Nếu bạn có một công nhân, anh ta phải đặt viên gạch từng người một cho đến khi anh ấy làm một bức tường, sau đó làm tương tự cho bức tường tiếp theo, và cứ thế cho đến khi tất cả các bức tường được xây dựng và kết nối. Đây là một nhiệm vụ chậm và mất thời gian có thể hưởng lợi từ việc song song.

Các đúng cách để làm điều này sẽ được parallelize tòa nhà tường - thuê, nói, thêm 3 người lao động, và có mỗi công nhân xây dựng tường riêng của mình để 4 bức tường có thể được xây dựng cùng một lúc. Thời gian cần để tìm thêm 3 công nhân và phân công nhiệm vụ của họ là không đáng kể so với số tiền tiết kiệm bạn nhận được bằng cách tăng 4 bức tường trong khoảng thời gian mà trước đây bạn phải thực hiện để xây dựng 1.

sai cách để làm điều đó là để song song các gạch đặt - thuê thêm một nghìn công nhân và mỗi công nhân chịu trách nhiệm đặt một viên gạch tại một thời điểm. Bạn có thể nghĩ, "Nếu một công nhân có thể đặt 2 viên gạch mỗi phút, thì một nghìn công nhân sẽ có thể đặt 2000 viên gạch mỗi phút, vì vậy tôi sẽ hoàn thành công việc này một cách nhanh chóng!" Nhưng thực tế là bằng cách song song khối lượng công việc của bạn ở cấp độ vi mô như vậy, bạn đang lãng phí một lượng lớn năng lượng thu thập và điều phối tất cả công nhân của bạn, giao nhiệm vụ cho họ ("đặt viên gạch này ngay tại đây"), đảm bảo không có ai công việc được can thiệp với bất cứ ai khác, vv

Vì vậy, đạo đức của sự giống nhau là: nói chung, việc sử dụng song song để chia ra các đơn vị nghiên cứu đầy (như tường), nhưng để lại mong manh đơn vị (như gạch) để được xử lý theo cách tuần tự thông thường.


* Vì lý do này, bạn thực sự có thể làm cho một xấp xỉ khá tốt của đạt được hiệu suất của song song trong bối cảnh công việc chuyên sâu hơn bằng cách tham gia bất kỳ mã nhanh thực hiện và bổ sung Thread.Sleep(100) (hoặc một số số ngẫu nhiên khác) đến cuối của nó. Việc thực thi tuần tự đột ngột của mã này sẽ bị chậm lại 100 ms mỗi lần lặp lại, trong khi các lệnh thực thi song song sẽ bị chậm lại đáng kể.

1

Điều quan trọng hơn mà tôi không thấy được đề cập là .AsParallel sẽ có hiệu suất khác nhau tùy thuộc vào bộ sưu tập được sử dụng.

Trong các thử nghiệm của tôi PLINQ là nhanh hơn LINQ khi KHÔNG sử dụng trên IEnumerable (Enumerable.Repeat):

29ms PLINQ ParralelQuery  
    30ms LINQ ParralelQuery  
    30ms PLINQ Array 
    38ms PLINQ List  
163ms LINQ IEnumerable 
211ms LINQ Array 
213ms LINQ List 
273ms PLINQ IEnumerable 
4 processors 

Mã là trong VB, nhưng cung cấp để chứng minh rằng sử dụng .ToArray đưa ra phiên bản PLINQ nhanh hơn vài lần

Dim test = Function(LINQ As Action, PLINQ As Action, type As String) 
        Dim sw1 = Stopwatch.StartNew : LINQ() : Dim ts1 = sw1.ElapsedMilliseconds 
        Dim sw2 = Stopwatch.StartNew : PLINQ() : Dim ts2 = sw2.ElapsedMilliseconds 
        Return {String.Format("{0,4}ms LINQ {1}", ts1, type), String.Format("{0,4}ms PLINQ {1}", ts2, type)} 
       End Function 

    Dim results = New List(Of String) From {Environment.ProcessorCount & " processors"} 
    Dim count = 12345678, iList = Enumerable.Repeat(1, count) 

    With iList : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "IEnumerable")) : End With 
    With iList.ToArray : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "Array")) : End With 
    With iList.ToList : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "List")) : End With 
    With ParallelEnumerable.Repeat(1, count) : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "ParralelQuery")) : End With 

    MessageBox.Show(String.join(Environment.NewLine, From l In results Order By l)) 

Chạy các thử nghiệm theo thứ tự khác nhau sẽ có kết quả khác một chút, vì vậy chúng có thể giúp chúng di chuyển lên và xuống dễ dàng hơn một chút.

Các vấn đề liên quan