Những mối nguy hiểm khi tạo một chủ đề với kích thước ngăn xếp là 50x mặc định là gì?


228

Tôi hiện đang làm việc trên một chương trình quan trọng về hiệu năng và một con đường tôi quyết định khám phá có thể giúp giảm mức tiêu thụ tài nguyên là tăng kích thước ngăn xếp của các luồng công nhân của tôi để tôi có thể di chuyển hầu hết các dữ liệu float[]mà tôi sẽ tham gia ngăn xếp (sử dụng stackalloc).

Tôi đã đọc rằng kích thước ngăn xếp mặc định cho một luồng là 1 MB, vì vậy để di chuyển tất cả float[], tôi sẽ phải mở rộng ngăn xếp khoảng 50 lần (đến 50 MB ~).

Tôi hiểu rằng điều này thường được coi là "không an toàn" và không được khuyến nghị, nhưng sau khi điểm chuẩn mã hiện tại của tôi theo phương pháp này, tôi đã phát hiện ra tốc độ xử lý tăng 530% ! Vì vậy, tôi không thể đơn giản vượt qua tùy chọn này mà không cần điều tra thêm, điều này dẫn tôi đến câu hỏi của tôi; các mối nguy hiểm liên quan đến việc tăng ngăn xếp lên kích thước lớn như vậy (điều gì có thể xảy ra) và tôi nên thực hiện các biện pháp phòng ngừa nào để giảm thiểu các nguy hiểm đó?

Mã kiểm tra của tôi,

public static unsafe void TestMethod1()
{
    float* samples = stackalloc float[12500000];

    for (var ii = 0; ii < 12500000; ii++)
    {
        samples[ii] = 32768;
    }
}

public static void TestMethod2()
{
    var samples = new float[12500000];

    for (var i = 0; i < 12500000; i++)
    {
        samples[i] = 32768;
    }
}

98
+1. Nghiêm túc. Bạn hỏi những gì LOOKS Giống như một câu hỏi ngu ngốc ngoài định mức và sau đó bạn đưa ra một trường hợp RẤT tốt mà trong kịch bản cụ thể của bạn, đó là một điều hợp lý để xem xét bởi vì bạn đã làm bài tập về nhà và đo lường kết quả. Điều này RẤT tốt - tôi nhớ điều đó với nhiều câu hỏi. Rất hay - thật tốt khi bạn xem xét một cái gì đó như thế này, thật đáng buồn là nhiều lập trình viên C # không nhận thức được những cơ hội tối ưu hóa đó. Có, thường không cần thiết - nhưng đôi khi nó rất quan trọng và tạo ra sự khác biệt.
TomTom

5
Tôi muốn thấy hai mã có tốc độ xử lý chênh lệch 530%, chỉ dựa trên việc di chuyển mảng sang ngăn xếp. Điều đó không cảm thấy đúng.
Dialecticus

13
Trước khi bạn nhảy xuống con đường đó: bạn đã thử sử dụng Marshal.AllocHGlobal(đừng quên FreeHGlobalquá) để phân bổ dữ liệu ngoài bộ nhớ được quản lý? Sau đó bỏ con trỏ đến a float*, và bạn sẽ được sắp xếp.
Marc Gravell

2
Nó cảm thấy đúng nếu bạn thực hiện nhiều phân bổ. Stackalloc bỏ qua tất cả các vấn đề về GC cũng có thể tạo / không tạo ra một địa phương rất mạnh ở cấp độ bộ xử lý. Đây là một trong những điều mà chiếc mũ trông giống như tối ưu hóa vi mô - trừ khi bạn viết chương trình toán học hiệu năng cao và có chính xác hành vi này và nó tạo ra sự khác biệt;)
TomTom

6
Sự nghi ngờ của tôi: một trong những phương pháp này kích hoạt giới hạn - kiểm tra trên mỗi lần lặp lại trong khi phương pháp kia thì không, hoặc nó được tối ưu hóa.
pjc50

Câu trả lời:


45

Khi so sánh mã kiểm tra với Sam, tôi xác định rằng cả hai chúng tôi đều đúng!
Tuy nhiên, về những điều khác nhau:

  • Truy cập bộ nhớ (đọc và viết) cũng nhanh như mọi lúc mọi nơi - stack, global hoặc heap.
  • Phân bổ nó, tuy nhiên, là nhanh nhất trên stack và chậm nhất trên heap.

Nó đi như thế này: stack< global< heap. (thời gian phân bổ)
Về mặt kỹ thuật, phân bổ ngăn xếp không thực sự là phân bổ, thời gian chạy chỉ đảm bảo một phần của ngăn xếp (khung?) được dành riêng cho mảng.

Tôi khuyên bạn nên cẩn thận với điều này, mặc dù.
Tôi khuyên bạn nên như sau:

  1. Khi bạn cần tạo các mảng thường xuyên mà không bao giờ rời khỏi hàm (ví dụ: bằng cách chuyển tham chiếu của nó), sử dụng ngăn xếp sẽ là một cải tiến rất lớn.
  2. Nếu bạn có thể tái chế một mảng, hãy làm như vậy bất cứ khi nào bạn có thể! Heap là nơi tốt nhất để lưu trữ đối tượng lâu dài. (gây ô nhiễm bộ nhớ toàn cầu không đẹp; khung stack có thể biến mất)

( Lưu ý : 1. chỉ áp dụng cho các loại giá trị; các loại tham chiếu sẽ được phân bổ trên heap và lợi ích sẽ giảm xuống 0)

Để tự trả lời câu hỏi: Tôi chưa gặp phải bất kỳ vấn đề nào với bất kỳ bài kiểm tra ngăn xếp lớn nào.
Tôi tin rằng vấn đề duy nhất có thể xảy ra là tràn ngăn xếp, nếu bạn không cẩn thận với các lệnh gọi chức năng và hết bộ nhớ khi tạo (các) luồng của mình nếu hệ thống sắp hết.

Phần dưới đây là câu trả lời ban đầu của tôi. Đó là sai-ish và các bài kiểm tra không đúng. Nó chỉ được giữ lại để tham khảo.


Thử nghiệm của tôi cho thấy bộ nhớ được cấp phát ngăn xếp và bộ nhớ chung chậm hơn ít nhất 15% so với (chiếm 120% thời gian) bộ nhớ được phân bổ heap để sử dụng trong các mảng!

Đây là mã thử nghiệm của tôi và đây là đầu ra mẫu:

Stack-allocated array time: 00:00:00.2224429
Globally-allocated array time: 00:00:00.2206767
Heap-allocated array time: 00:00:00.1842670
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 100.80 %| 120.72 %|
--+---------+---------+---------+
G |  99.21 %|    -    | 119.76 %|
--+---------+---------+---------+
H |  82.84 %|  83.50 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

Tôi đã thử nghiệm trên Windows 8.1 Pro (với Bản cập nhật 1), sử dụng i7 4700 MQ, trong .NET 4.5.1
Tôi đã thử nghiệm cả với x86 và x64 và kết quả là giống hệt nhau.

Chỉnh sửa : Tôi đã tăng kích thước ngăn xếp của tất cả các luồng 201 MB, kích thước mẫu lên 50 triệu và giảm số lần lặp xuống còn 5.
Kết quả giống như trên :

Stack-allocated array time: 00:00:00.4504903
Globally-allocated array time: 00:00:00.4020328
Heap-allocated array time: 00:00:00.3439016
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 112.05 %| 130.99 %|
--+---------+---------+---------+
G |  89.24 %|    -    | 116.90 %|
--+---------+---------+---------+
H |  76.34 %|  85.54 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

Mặc dù, có vẻ như ngăn xếp thực sự đang trở nên chậm hơn .


Tôi phải không đồng ý, theo kết quả của điểm chuẩn của tôi (xem bình luận ở cuối trang để biết kết quả) cho thấy rằng ngăn xếp nhanh hơn một chút so với toàn cầu và nhanh hơn nhiều so với đống; và để chắc chắn rằng kết quả của tôi là chính xác, tôi đã chạy thử nghiệm 20 lần và mỗi phương pháp được gọi 100 lần cho mỗi lần lặp thử nghiệm. Bạn có chắc chắn chạy điểm chuẩn của bạn một cách chính xác?
Sam

Tôi đang nhận được kết quả rất không nhất quán. Với sự tin tưởng hoàn toàn, x64, cấu hình phát hành, không có trình gỡ lỗi, tất cả chúng đều nhanh như nhau (chênh lệch dưới 1%; dao động) trong khi của bạn thực sự nhanh hơn nhiều với một ngăn xếp. Tôi cần kiểm tra thêm! Chỉnh sửa : Bạn NÊN ném một ngoại lệ ngăn xếp tràn. Bạn chỉ phân bổ đủ cho mảng. O_o
Vercas

Vâng tôi biết, nó gần rồi. Bạn cần lặp lại điểm chuẩn một vài lần, như tôi đã làm, có thể thử thực hiện trung bình hơn 5 lần chạy.
Sam

1
@Voo Lần chạy đầu tiên mất nhiều thời gian như lần chạy thứ 100 của bất kỳ bài kiểm tra nào đối với tôi. Từ kinh nghiệm của tôi, điều JIT Java này hoàn toàn không áp dụng cho .NET. Việc "làm nóng" duy nhất mà .NET thực hiện là tải các lớp và tập hợp khi được sử dụng lần đầu tiên.
Vercas

2
@Voo Kiểm tra điểm chuẩn của tôi và điểm từ ý chính mà anh ấy đã thêm trong một nhận xét cho câu trả lời này. Lắp ráp các mã với nhau và chạy một vài trăm thử nghiệm. Sau đó quay lại và báo cáo kết luận của bạn. Tôi đã thực hiện các bài kiểm tra của mình rất kỹ lưỡng và tôi biết rất rõ những gì tôi đang nói khi nói rằng .NET không diễn giải bất kỳ mã byte nào giống như Java, nó JITs ngay lập tức.
Vercas

28

Tôi đã phát hiện ra tốc độ xử lý tăng 530%!

Đó là mối nguy hiểm lớn nhất mà tôi muốn nói. Có một cái gì đó sai nghiêm trọng với điểm chuẩn của bạn, mã hành xử không thể đoán trước này thường có một lỗi khó chịu ẩn ở đâu đó.

Rất, rất khó để tiêu thụ nhiều dung lượng ngăn xếp trong một chương trình .NET, ngoài việc đệ quy quá mức. Kích thước của khung ngăn xếp của các phương thức được quản lý được đặt trong đá. Đơn giản là tổng các đối số của phương thức và các biến cục bộ trong một phương thức. Trừ đi những cái có thể được lưu trữ trong một thanh ghi CPU, bạn có thể bỏ qua điều đó vì có rất ít trong số chúng.

Tăng kích thước ngăn xếp không hoàn thành bất cứ điều gì, bạn sẽ chỉ dành một loạt không gian địa chỉ sẽ không bao giờ được sử dụng. Tất nhiên, không có cơ chế nào có thể giải thích sự gia tăng hoàn hảo từ việc không sử dụng bộ nhớ.

Điều này không giống như một chương trình gốc, đặc biệt là một chương trình được viết bằng C, nó cũng có thể dành chỗ cho các mảng trên khung ngăn xếp. Các vector tấn công phần mềm độc hại cơ bản đằng sau bộ đệm ngăn xếp tràn. Cũng có thể trong C #, bạn phải sử dụng stackalloctừ khóa. Nếu bạn đang làm điều đó thì mối nguy hiểm rõ ràng là phải viết mã không an toàn chịu các cuộc tấn công như vậy, cũng như tham nhũng khung ngăn xếp ngẫu nhiên. Rất khó chẩn đoán lỗi. Có một biện pháp chống lại điều này trong các jitter sau này, tôi nghĩ bắt đầu từ .NET 4.0, trong đó jitter tạo mã để đặt "cookie" vào khung stack và kiểm tra xem nó có còn nguyên vẹn khi phương thức quay lại không. Sự cố ngay lập tức đến máy tính để bàn mà không có cách nào để chặn hoặc báo cáo sự cố nếu điều đó xảy ra. Điều đó ... nguy hiểm cho trạng thái tinh thần của người dùng.

Chuỗi chính của chương trình của bạn, chuỗi được khởi động bởi hệ điều hành, sẽ có ngăn xếp 1 MB theo mặc định, 4 MB khi bạn biên dịch chương trình nhắm mục tiêu x64. Việc tăng yêu cầu chạy Editbin.exe với tùy chọn / STACK trong sự kiện xây dựng bài đăng. Thông thường, bạn có thể yêu cầu tối đa 500 MB trước khi chương trình của bạn gặp sự cố khi bắt đầu khi chạy ở chế độ 32 bit. Tất nhiên, các chủ đề cũng có thể dễ dàng hơn nhiều, vùng nguy hiểm thường dao động khoảng 90 MB cho chương trình 32 bit. Kích hoạt khi chương trình của bạn đã chạy trong một thời gian dài và không gian địa chỉ bị phân mảnh từ các phân bổ trước đó. Tổng mức sử dụng không gian địa chỉ phải cao, trên một gig, để có được chế độ thất bại này.

Kiểm tra lại mã của bạn, có gì đó rất sai. Bạn không thể tăng tốc x5 với ngăn xếp lớn hơn trừ khi bạn viết mã rõ ràng để tận dụng lợi thế của nó. Mà luôn luôn yêu cầu mã không an toàn. Sử dụng các con trỏ trong C # luôn có một mẹo để tạo mã nhanh hơn, nó không chịu sự kiểm tra giới hạn mảng.


21
Việc tăng tốc 5x được báo cáo là từ việc chuyển từ float[]sang float*. Các ngăn xếp lớn chỉ đơn giản là làm thế nào được thực hiện. Tăng tốc x5 trong một số tình huống là hoàn toàn hợp lý cho sự thay đổi đó.
Marc Gravell

3
Được rồi, tôi chưa có đoạn mã khi tôi bắt đầu trả lời câu hỏi. Vẫn đủ gần.
Hans Passant

22

Tôi sẽ có một bảo lưu ở đó mà đơn giản là tôi sẽ không biết cách dự đoán nó - quyền, GC (cần quét ngăn xếp), v.v. - tất cả đều có thể bị ảnh hưởng. Thay vào đó, tôi sẽ rất muốn sử dụng bộ nhớ không được quản lý:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}

1
Câu hỏi bên lề: Tại sao GC cần quét stack? Bộ nhớ được phân bổ bởi stackallockhông phải là bộ sưu tập rác.
dcastro

6
@dcastro nó cần quét ngăn xếp để kiểm tra các tham chiếu chỉ tồn tại trên ngăn xếp. Tôi chỉ đơn giản là không biết nó sẽ làm gì khi nó trở nên to lớn như vậy stackalloc- nó cần phải nhảy nó, và bạn hy vọng nó sẽ làm được điều đó một cách dễ dàng - nhưng điểm tôi đang cố gắng thực hiện là nó giới thiệu các biến chứng / lo lắng không cần thiết . IMO, stackalloctuyệt vời như một bộ đệm đầu, nhưng đối với một không gian làm việc chuyên dụng, dự kiến ​​sẽ chỉ phân bổ một bộ nhớ chunk ở đâu đó, thay vì lạm dụng / nhầm lẫn ngăn xếp,
Marc Gravell

8

Một điều có thể sai là bạn có thể không được phép làm như vậy. Trừ khi chạy ở chế độ tin cậy hoàn toàn, Framework sẽ bỏ qua yêu cầu về kích thước ngăn xếp lớn hơn (xem MSDN trên Thread Constructor (ParameterizedThreadStart, Int32))

Thay vì tăng kích thước ngăn xếp hệ thống lên số lượng lớn như vậy, tôi khuyên bạn nên viết lại mã của mình để nó sử dụng Lặp lại và thực hiện ngăn xếp thủ công trên heap.


1
Thay vào đó, tôi sẽ lặp đi lặp lại. Ngoài ra, mã của tôi đang chạy ở chế độ tin cậy hoàn toàn, vậy có điều gì khác tôi nên tìm kiếm không?
Sam

6

Các mảng hiệu suất cao có thể có thể truy cập theo cách tương tự như một C # bình thường nhưng đó có thể là khởi đầu của rắc rối: Hãy xem xét đoạn mã sau:

float[] someArray = new float[100]
someArray[200] = 10.0;

Bạn mong đợi một ngoại lệ bị ràng buộc và điều này hoàn toàn có ý nghĩa bởi vì bạn đang cố truy cập phần tử 200 nhưng giá trị tối đa được phép là 99. Nếu bạn đi đến tuyến stackalloc thì sẽ không có đối tượng nào quấn quanh mảng của bạn để kiểm tra ràng buộc và sau đây sẽ không hiển thị bất kỳ ngoại lệ:

Float* pFloat =  stackalloc float[100];
fFloat[200]= 10.0;

Ở trên bạn đang phân bổ đủ bộ nhớ để chứa 100 phao và bạn đang đặt vị trí bộ nhớ sizeof (float) bắt đầu tại vị trí bắt đầu của bộ nhớ này + 200 * sizeof (float) để giữ giá trị float của bạn 10. Không có gì ngạc nhiên khi bộ nhớ này nằm ngoài bộ nhớ được phân bổ cho các float và không ai có thể biết những gì có thể được lưu trữ trong địa chỉ đó. Nếu bạn may mắn, bạn có thể đã sử dụng một số bộ nhớ hiện chưa sử dụng nhưng đồng thời có khả năng bạn có thể ghi đè lên một số vị trí được sử dụng để lưu trữ các biến khác. Để tóm tắt: Hành vi thời gian chạy không thể đoán trước.


Thực tế sai. Các bài kiểm tra thời gian chạy và trình biên dịch vẫn còn đó.
TomTom

9
@TomTom erm, không; Câu trả lời có công; câu hỏi nói vềstackalloc , trong trường hợp chúng ta đang nói về float*vv - không có cùng kiểm tra. Nó được gọi là unsafemột lý do rất tốt. Cá nhân tôi hoàn toàn hài lòng unsafekhi sử dụng khi có lý do chính đáng, nhưng Socrates đưa ra một số điểm hợp lý.
Marc Gravell

@Marc Đối với mã được hiển thị (sau khi JIT được chạy), không có kiểm tra giới hạn nào nữa bởi vì trình biên dịch không quan trọng vì lý do tất cả các truy cập đều nằm trong giới hạn. Nói chung mặc dù điều này chắc chắn có thể làm cho một sự khác biệt.
Voo

6

Các ngôn ngữ vi điểm đánh dấu bằng JIT và GC như Java hoặc C # có thể hơi phức tạp, do đó, nói chung nên sử dụng một khung công tác hiện có - Java cung cấp mhf hoặc Caliper rất tuyệt vời, theo hiểu biết của tôi, C # không cung cấp bất cứ điều gì tiếp cận những người. Jon Skeet đã viết điều này ở đây mà tôi sẽ giả định một cách mù quáng sẽ quan tâm đến những điều quan trọng nhất (Jon biết những gì anh ta đang làm trong khu vực đó; cũng không phải lo lắng tôi đã thực sự kiểm tra). Tôi đã điều chỉnh thời gian một chút vì 30 giây cho mỗi lần kiểm tra sau khi khởi động quá nhiều so với sự kiên nhẫn của tôi (5 giây nên làm).

Vì vậy, trước tiên, kết quả, .NET 4.5.1 trong Windows 7 x64 - các con số biểu thị các lần lặp mà nó có thể chạy trong 5 giây nên cao hơn là tốt hơn.

x64 JIT:

Standard       10,589.00  (1.00)
UnsafeStandard 10,612.00  (1.00)
Stackalloc     12,088.00  (1.14)
FixedStandard  10,715.00  (1.01)
GlobalAlloc    12,547.00  (1.18)

x86 JIT (vâng, điều đó vẫn còn buồn):

Standard       14,787.00   (1.02)
UnsafeStandard 14,549.00   (1.00)
Stackalloc     15,830.00   (1.09)
FixedStandard  14,824.00   (1.02)
GlobalAlloc    18,744.00   (1.29)

Điều này mang lại tốc độ tăng tốc hợp lý hơn nhiều nhất là 14% (và phần lớn chi phí hoạt động là do GC phải chạy, coi đó là một trường hợp xấu nhất trong thực tế). Kết quả x86 rất thú vị - không hoàn toàn rõ ràng những gì đang diễn ra ở đó.

và đây là mã:

public static float Standard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float UnsafeStandard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float Stackalloc(int size) {
    float* samples = stackalloc float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float FixedStandard(int size) {
    float[] prev = new float[size];
    fixed (float* samples = &prev[0]) {
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    }
}

public static unsafe float GlobalAlloc(int size) {
    var ptr = Marshal.AllocHGlobal(size * sizeof(float));
    try {
        float* samples = (float*)ptr;
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    } finally {
        Marshal.FreeHGlobal(ptr);
    }
}

static void Main(string[] args) {
    int inputSize = 100000;
    var results = TestSuite.Create("Tests", inputSize, Standard(inputSize)).
        Add(Standard).
        Add(UnsafeStandard).
        Add(Stackalloc).
        Add(FixedStandard).
        Add(GlobalAlloc).
        RunTests();
    results.Display(ResultColumns.NameAndIterations);
}

Một quan sát thú vị, tôi sẽ phải kiểm tra lại điểm chuẩn của mình. Mặc dù điều này vẫn không thực sự trả lời câu hỏi của tôi, " ... những mối nguy hiểm liên quan đến việc tăng ngăn xếp lên kích thước lớn như vậy ... ". Ngay cả khi kết quả của tôi không chính xác, câu hỏi vẫn còn hiệu lực; Tôi đánh giá cao những nỗ lực tuy nhiên.
Sam

1
@Sam Khi sử dụng 12500000như kích thước, tôi thực sự có một ngoại lệ stackoverflow. Nhưng chủ yếu là về việc từ chối tiền đề cơ bản rằng sử dụng mã được cấp phát ngăn xếp là một số đơn đặt hàng có cường độ nhanh hơn. Chúng tôi đang làm khá nhiều công việc ít nhất có thể ở đây nếu không và sự khác biệt chỉ là khoảng 10 - 15% - trong thực tế, nó sẽ còn thấp hơn nữa .. điều này theo tôi chắc chắn thay đổi toàn bộ cuộc thảo luận.
Voo

5

Vì sự khác biệt hiệu suất là quá lớn, vấn đề hầu như không liên quan đến phân bổ. Nó có khả năng gây ra bởi truy cập mảng.

Tôi đã tháo rời phần thân của các hàm:

TestMethod1:

IL_0011:  ldloc.0 
IL_0012:  ldloc.1 
IL_0013:  ldc.i4.4 
IL_0014:  mul 
IL_0015:  add 
IL_0016:  ldc.r4 32768.
IL_001b:  stind.r4 // <----------- This one
IL_001c:  ldloc.1 
IL_001d:  ldc.i4.1 
IL_001e:  add 
IL_001f:  stloc.1 
IL_0020:  ldloc.1 
IL_0021:  ldc.i4 12500000
IL_0026:  blt IL_0011

TestMethod2:

IL_0012:  ldloc.0 
IL_0013:  ldloc.1 
IL_0014:  ldc.r4 32768.
IL_0019:  stelem.r4 // <----------- This one
IL_001a:  ldloc.1 
IL_001b:  ldc.i4.1 
IL_001c:  add 
IL_001d:  stloc.1 
IL_001e:  ldloc.1 
IL_001f:  ldc.i4 12500000
IL_0024:  blt IL_0012

Chúng tôi có thể kiểm tra việc sử dụng hướng dẫn và quan trọng hơn, ngoại lệ họ ném vào thông số ECMA :

stind.r4: Store value of type float32 into memory at address

Ngoại lệ nó ném:

System.NullReferenceException

stelem.r4: Replace array element at index with the float32 value on the stack.

Ngoại lệ nó ném:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

Như bạn có thể thấy, stelemcó nhiều công việc hơn trong kiểm tra phạm vi mảng và kiểm tra kiểu. Vì thân vòng lặp làm rất ít việc (chỉ gán giá trị), nên chi phí kiểm tra chi phối thời gian tính toán. Vì vậy, đó là lý do tại sao hiệu suất khác nhau 530%.

Và điều này cũng trả lời câu hỏi của bạn: mối nguy hiểm là sự vắng mặt của phạm vi mảng & kiểm tra kiểu. Điều này không an toàn (như đã đề cập trong phần khai báo hàm; D).


4

EDIT: (thay đổi nhỏ trong mã và trong đo lường tạo ra thay đổi lớn trong kết quả)

Đầu tiên tôi chạy mã được tối ưu hóa trong trình gỡ lỗi (F5) nhưng điều đó đã sai. Nó nên được chạy mà không cần trình gỡ lỗi (Ctrl + F5). Thứ hai, mã có thể được tối ưu hóa triệt để, vì vậy chúng tôi phải làm phức tạp nó để trình tối ưu hóa không gây rối với phép đo của chúng tôi. Tôi đã thực hiện tất cả các phương thức trả về một mục cuối cùng trong mảng và mảng được điền khác nhau. Ngoài ra, có thêm một số 0 trong OP TestMethod2luôn làm cho nó chậm hơn mười lần.

Tôi đã thử một số phương pháp khác, ngoài hai phương pháp mà bạn cung cấp. Phương thức 3 có cùng mã với phương thức 2 của bạn, nhưng hàm được khai báo unsafe. Phương pháp 4 là sử dụng truy cập con trỏ vào mảng được tạo thường xuyên. Phương pháp 5 đang sử dụng truy cập con trỏ vào bộ nhớ không được quản lý, như được mô tả bởi Marc Gravell. Tất cả năm phương pháp chạy trong thời gian rất giống nhau. M5 là nhanh nhất (và M1 là thứ hai gần). Sự khác biệt giữa nhanh nhất và chậm nhất là khoảng 5%, đó không phải là điều tôi sẽ quan tâm.

    public static unsafe float TestMethod3()
    {
        float[] samples = new float[5000000];

        for (var ii = 0; ii < 5000000; ii++)
        {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }

        return samples[5000000 - 1];
    }

    public static unsafe float TestMethod4()
    {
        float[] prev = new float[5000000];
        fixed (float* samples = &prev[0])
        {
            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
    }

    public static unsafe float TestMethod5()
    {
        var ptr = Marshal.AllocHGlobal(5000000 * sizeof(float));
        try
        {
            float* samples = (float*)ptr;

            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
        finally
        {
            Marshal.FreeHGlobal(ptr);
        }
    }

Vậy M3 có giống như M2 chỉ được đánh dấu là "không an toàn"? Khá nghi ngờ rằng nó sẽ nhanh hơn ... bạn có chắc không?
Roman Starkov

@romkyns Tôi vừa mới chạy điểm chuẩn (M2 so với M3) và đáng ngạc nhiên là M3 thực sự nhanh hơn 2,14% so với M2.
Sam

" Kết luận là không cần sử dụng ngăn xếp. " Khi phân bổ các khối lớn như tôi đã đưa ra trong bài đăng của mình, tôi đồng ý, nhưng, sau khi vừa hoàn thành một số điểm chuẩn hơn so với M2 (sử dụng ý tưởng của PFM cho cả hai phương pháp) phải không đồng ý, vì giờ đây M1 nhanh hơn 135% so với M2.
Sam

1
@Sam Nhưng bạn vẫn đang so sánh truy cập con trỏ với truy cập mảng! Đó là nguyên thủy những gì làm cho nó nhanh hơn. TestMethod4vs TestMethod1là một so sánh tốt hơn nhiều cho stackalloc.
Roman Starkov

@romkyns À đúng rồi, tôi quên mất điều đó; Tôi đã chạy lại điểm chuẩn , giờ chỉ còn chênh lệch 8% (M1 là nhanh hơn cả hai).
Sam
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.