Hiệu suất bất ngờ với các loại như là loại và loại nullable


330

Tôi chỉ sửa đổi chương 4 của C # in Depth liên quan đến các loại nullable và tôi đang thêm một phần về cách sử dụng toán tử "as", cho phép bạn viết:

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    ... // Use x.Value in here
}

Tôi nghĩ rằng điều này thực sự gọn gàng và nó có thể cải thiện hiệu suất so với tương đương C # 1, sử dụng "is" theo sau là một diễn viên - sau tất cả, cách này chúng ta chỉ cần yêu cầu kiểm tra loại động một lần, và sau đó kiểm tra giá trị đơn giản .

Điều này dường như không phải là trường hợp, tuy nhiên. Tôi đã bao gồm một ứng dụng thử nghiệm mẫu bên dưới, về cơ bản tổng hợp tất cả các số nguyên trong một mảng đối tượng - nhưng mảng chứa rất nhiều tham chiếu null và tham chiếu chuỗi cũng như các số nguyên được đóng hộp. Điểm chuẩn đo lường mã bạn phải sử dụng trong C # 1, mã sử dụng toán tử "as" và chỉ để khởi động giải pháp LINQ. Trước sự ngạc nhiên của tôi, mã C # 1 nhanh hơn 20 lần trong trường hợp này - và thậm chí mã LINQ (mà tôi dự kiến ​​sẽ chậm hơn, do các trình lặp có liên quan) đánh bại mã "dưới dạng".

Là triển khai .NET isinstcho các loại nullable thực sự chậm? Có phải nó là bổ sung unbox.anygây ra vấn đề? Có một lời giải thích khác cho điều này? Hiện tại tôi cảm thấy mình sẽ phải đưa ra một cảnh báo về việc sử dụng điều này trong các tình huống nhạy cảm về hiệu suất ...

Các kết quả:

Diễn viên: 10000000: 121
Như: 10000000: 2211
LINQ: 10000000: 2143

Mã số:

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

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i+1] = "";
            values[i+2] = 1;
        }

        FindSumWithCast(values);
        FindSumWithAs(values);
        FindSumWithLinq(values);
    }

    static void FindSumWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int) o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }

    static void FindSumWithAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }

    static void FindSumWithLinq(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = values.OfType<int>().Sum();
        sw.Stop();
        Console.WriteLine("LINQ: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }
}

8
Tại sao không nhìn vào mã jited? Ngay cả trình gỡ lỗi VS cũng có thể hiển thị nó.
Anton Tykhyy

2
Tôi chỉ tò mò thôi, bạn đã thử nghiệm với CLR 4.0 chưa?
Dirk Vollmar

1
@Anton: Điểm tốt. Sẽ có lúc nào đó (mặc dù điều này không có trong VS vào lúc này :) @divo: Vâng, và nó tệ hơn tất cả các vòng. Nhưng sau đó là bản beta, vì vậy có thể có rất nhiều mã gỡ lỗi trong đó.
Jon Skeet

1
Hôm nay tôi đã học bạn có thể sử dụng astrên các loại nullable. Thật thú vị, vì nó không thể được sử dụng trên các loại giá trị khác. Thật ra, đáng ngạc nhiên hơn.
leppie

3
@Lepp nó có ý nghĩa hoàn hảo cho nó để không làm việc trên các loại giá trị. Hãy suy nghĩ về nó, ascố gắng chuyển sang một loại và nếu thất bại thì nó trả về null. Bạn không thể đặt loại giá trị thành null
Earlz

Câu trả lời:


209

Rõ ràng mã máy mà trình biên dịch JIT có thể tạo cho trường hợp đầu tiên hiệu quả hơn nhiều. Một quy tắc thực sự có ích là một đối tượng chỉ có thể được bỏ hộp đến một biến có cùng loại với giá trị được đóng hộp. Điều đó cho phép trình biên dịch JIT tạo mã rất hiệu quả, không cần phải chuyển đổi giá trị.

Các thử nghiệm điều hành được dễ dàng, chỉ cần kiểm tra nếu đối tượng không phải là null và là loại dự kiến, mất nhưng một vài hướng dẫn mã máy. Việc truyền cũng dễ dàng, trình biên dịch JIT biết vị trí của các bit giá trị trong đối tượng và sử dụng chúng trực tiếp. Không có sao chép hoặc chuyển đổi xảy ra, tất cả các mã máy là nội tuyến và mất nhưng khoảng một tá hướng dẫn. Điều này cần phải thực sự hiệu quả trở lại trong .NET 1.0 khi quyền anh là phổ biến.

Đúc để int? mất nhiều công sức hơn Biểu diễn giá trị của số nguyên được đóng hộp không tương thích với bố cục bộ nhớ của Nullable<int>. Một chuyển đổi là cần thiết và mã là khó khăn do các loại enum đóng hộp có thể. Trình biên dịch JIT tạo một lệnh gọi đến hàm trợ giúp CLR có tên JIT_Unbox_Nullable để hoàn thành công việc. Đây là một hàm mục đích chung cho bất kỳ loại giá trị nào, rất nhiều mã ở đó để kiểm tra các loại. Và giá trị được sao chép. Khó có thể ước tính chi phí vì mã này bị khóa bên trong mscorwks.dll, nhưng có thể có hàng trăm hướng dẫn mã máy.

Phương thức mở rộng Linq OfType () cũng sử dụng toán tử is và cast. Tuy nhiên, đây là một kiểu đúc chung. Trình biên dịch JIT tạo một lệnh gọi đến hàm trợ giúp, JIT_Unbox () có thể thực hiện chuyển sang một loại giá trị tùy ý. Tôi không có một lời giải thích tuyệt vời tại sao nó lại chậm như các diễn viên Nullable<int>, vì cho rằng công việc ít hơn là cần thiết. Tôi nghi ngờ rằng ngen.exe có thể gây rắc rối ở đây.


16
Được rồi, tôi đã bị thuyết phục. Tôi đoán tôi đã từng nghĩ "là" có khả năng đắt đỏ vì khả năng đi lên một hệ thống phân cấp thừa kế - nhưng trong trường hợp của một loại giá trị, không có khả năng phân cấp, vì vậy nó có thể là một so sánh đơn giản theo bitwise . Tôi vẫn nghĩ rằng mã JIT cho trường hợp nullable có thể được JIT tối ưu hóa rất nhiều so với nó.
Jon Skeet

26

Dường như với tôi rằng isinstnó chỉ thực sự chậm trên các loại nullable. Trong phương pháp FindSumWithCasttôi đã thay đổi

if (o is int)

đến

if (o is int?)

Điều này cũng làm chậm đáng kể việc thực hiện. Sự khác biệt duy nhất trong IL tôi có thể thấy là

isinst     [mscorlib]System.Int32

được đổi thành

isinst     valuetype [mscorlib]System.Nullable`1<int32>

1
Nó còn hơn thế nữa; trong trường hợp "diễn viên", isinstsau đó là một bài kiểm tra về tính vô hiệu và sau đó có điều kiện là một unbox.any. Trong trường hợp vô giá trị có một điều kiện vô điều kiệnunbox.any .
Jon Skeet

Có, hóa ra cả hai isinstunbox.anychậm hơn trên các loại nullable.
Dirk Vollmar

@Jon: Bạn có thể xem lại câu trả lời của tôi về lý do tại sao diễn viên là cần thiết. (Tôi biết cái này đã cũ, nhưng tôi mới phát hiện ra cái q này và nghĩ rằng tôi nên cung cấp cho tôi 2c những gì tôi biết về CLR).
Julian Rudolph

22

Điều này ban đầu bắt đầu như một Nhận xét cho câu trả lời xuất sắc của Hans Passant, nhưng nó đã quá dài nên tôi muốn thêm một vài bit ở đây:

Đầu tiên, astoán tử C # sẽ phát ra một isinstlệnh IL ( istoán tử này cũng vậy ). (Một hướng dẫn thú vị khác là castclass, được phát ra khi bạn thực hiện truyền trực tiếp và trình biên dịch biết rằng việc kiểm tra thời gian chạy không thể bị dừng lại.)

Đây là những gì isinsthiện ( ECMA 335 Phân vùng III, 4.6 ):

Định dạng: isinst typeTok

typeTok là một thẻ siêu dữ liệu (một typeref, typedefhoặc typespec), cho thấy lớp mong muốn.

Nếu typeTok là một loại giá trị không thể rỗng hoặc một loại tham số chung thì nó được hiểu là typeTok của hộp được đóng hộp .

Nếu typeTok là một loại nullable, Nullable<T>thì nó được hiểu làT

Quan trọng nhất:

Nếu loại thực tế (không phải loại theo dõi xác minh) của obj có thể xác minh được gán cho loại typeTok thì isinstthành công và obj ( kết quả ) được trả về không thay đổi trong khi xác minh theo dõi loại của nó là typeTok . Không giống như các ép buộc (§1.6) và chuyển đổi (§3.27), isinstkhông bao giờ thay đổi loại thực tế của một đối tượng và bảo tồn danh tính đối tượng (xem Phân vùng I).

Vì vậy, kẻ giết người hiệu suất không phải isinsttrong trường hợp này, mà là bổ sung unbox.any. Điều này không rõ ràng từ câu trả lời của Hans, khi anh chỉ nhìn vào mã JITed. Nói chung, trình biên dịch C # sẽ phát ra unbox.anysau isinst T?(nhưng sẽ bỏ qua nó trong trường hợp bạn làm isinst T, khi nào Tlà kiểu tham chiếu).

Tại sao nó làm điều đó? isinst T?không bao giờ có tác dụng rõ ràng, tức là bạn lấy lại a T?. Thay vào đó, tất cả các hướng dẫn này đảm bảo rằng bạn có một "boxed T"hộp có thể được bỏ hộp T?. Để có được một thực tế T?, chúng ta vẫn cần hủy hộp thư "boxed T"đến T?, đó là lý do tại sao trình biên dịch phát ra unbox.anysau isinst. Nếu bạn nghĩ về nó, điều này có ý nghĩa bởi vì "định dạng hộp" T?chỉ là một "boxed T"và thực hiện castclassisinstthực hiện unbox sẽ không nhất quán.

Sao lưu phát hiện của Hans với một số thông tin từ tiêu chuẩn , đây là:

(Phân vùng ECMA 335 III, 4.33): unbox.any

Khi được áp dụng cho dạng đóng hộp của một loại giá trị, unbox.anylệnh sẽ trích xuất giá trị được chứa trong obj (của loại O). (Nó tương đương với unboxtheo sau ldobj.) Khi được áp dụng cho một loại tham chiếu, unbox.anyhướng dẫn có tác dụng tương tự như castclasstypeTok.

(Phân vùng ECMA 335 III, 4.32): unbox

Thông thường, unboxchỉ cần tính toán địa chỉ của loại giá trị đã có bên trong đối tượng được đóng hộp. Cách tiếp cận này là không thể khi bỏ hộp các loại giá trị nullable. Vì Nullable<T>các giá trị được chuyển đổi thành hộp Tstrong quá trình vận hành hộp, nên việc triển khai thường phải tạo mới Nullable<T>trên heap và tính địa chỉ cho đối tượng mới được phân bổ.


Tôi nghĩ rằng câu trích dẫn cuối cùng có thể có một lỗi đánh máy; không nên “... trên đống ...” được “trên thực hiện ngăn xếp ?” Có vẻ như unboxing trở lại vào một số trường hợp heap GC mới hoán đổi vấn đề ban đầu cho một vấn đề mới gần như giống hệt nhau.
Glenn Slayden

19

Thật thú vị, tôi đã chuyển thông tin phản hồi về hỗ trợ của nhà điều hành thông qua dynamicviệc đặt hàng chậm hơn Nullable<T>(tương tự như thử nghiệm đầu tiên này ) - tôi nghi ngờ vì những lý do rất giống nhau.

Phải yêu Nullable<T>. Một điều thú vị khác là mặc dù các điểm JIT (và loại bỏ) nullđối với các cấu trúc không có giá trị, nhưng nó cho phép Nullable<T>:

using System;
using System.Diagnostics;
static class Program {
    static void Main() { 
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}

Yowser. Đó là một sự khác biệt thực sự đau đớn. Eek.
Jon Skeet

Nếu không có thứ tốt nào khác xuất hiện trong tất cả những điều này, thì nó đã khiến tôi phải đưa ra các cảnh báo cho cả mã gốc của mình mã này :)
Jon Skeet

Tôi biết đây là một câu hỏi cũ, nhưng bạn có thể giải thích ý của bạn bằng cách "các điểm JIT (và loại bỏ) nullcho các cấu trúc không nullable" không? Bạn có nghĩa là nó thay thế nullbằng một giá trị mặc định hoặc một cái gì đó trong thời gian chạy?
Justin Morgan

2
@Justin - một phương thức chung có thể được sử dụng trong thời gian chạy với bất kỳ số lượng hoán vị của các tham số chung ( Tvv). Các yêu cầu ngăn xếp vv phụ thuộc vào các đối số (lượng không gian ngăn xếp cho một cục bộ, v.v.), do đó bạn nhận được một JIT cho bất kỳ hoán vị duy nhất nào liên quan đến một loại giá trị. Tuy nhiên, các tài liệu tham khảo đều có cùng kích thước, vì vậy hãy chia sẻ JIT. Trong khi thực hiện JIT loại trên mỗi giá trị, nó có thể kiểm tra một vài tình huống rõ ràng và cố gắng loại bỏ mã không thể truy cập do những thứ như null không thể. Nó không hoàn hảo, lưu ý. Ngoài ra, tôi đang bỏ qua AOT cho các bên trên.
Marc Gravell

Kiểm tra nullable không giới hạn vẫn chậm hơn 2,5 bậc, nhưng có một số tối ưu hóa đang diễn ra khi bạn không sử dụng countbiến. Việc thêm vào Console.Write(count.ToString()+" ");sau watch.Stop();cả hai trường hợp làm chậm các thử nghiệm khác chỉ bằng một mức độ lớn, nhưng thử nghiệm nullable không giới hạn không bị thay đổi. Lưu ý rằng cũng có những thay đổi khi bạn kiểm tra các trường hợp khi nullđược thông qua, xác nhận mã gốc không thực sự thực hiện kiểm tra null và gia tăng cho các kiểm tra khác. Linqpad
Mark Hurd

12

Đây là kết quả của FindSumWithAsAndHas ở trên: văn bản thay thế

Đây là kết quả của FindSumWithCast: văn bản thay thế

Kết quả:

  • Sử dụng as, nó sẽ kiểm tra đầu tiên nếu một đối tượng là một thể hiện của Int32; bên dưới mui xe nó đang sử dụng isinst Int32(tương tự như mã viết tay: if (o là int)). Và sử dụng as, nó cũng vô điều kiện unbox đối tượng. Và đó là một kẻ giết người hiệu suất thực sự để gọi một tài sản (nó vẫn là một chức năng dưới mui xe), IL_0027

  • Sử dụng cast, bạn kiểm tra trước nếu đối tượng là một int if (o is int); dưới mui xe này đang sử dụng isinst Int32. Nếu đó là một thể hiện của int, thì bạn có thể bỏ hộp một cách an toàn, IL_002D

Nói một cách đơn giản, đây là mã giả sử dụng asphương pháp:

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    

Và đây là mã giả sử dụng phương pháp đúc:

if (o isinst Int32)
    sum += (o unbox Int32)

Vì vậy, diễn viên ( (int)a[i], cũng là cú pháp trông giống như một diễn viên, nhưng thực ra nó là unboxing, cast và unboxing chia sẻ cùng một cú pháp, lần sau tôi sẽ là phương pháp mô phạm với thuật ngữ đúng) thực sự nhanh hơn, bạn chỉ cần mở hộp một giá trị khi một đối tượng được quyết định một int. Điều tương tự không thể nói với việc sử dụng một ascách tiếp cận.


11

Để giữ cho câu trả lời này được cập nhật, điều đáng nói là hầu hết các cuộc thảo luận trên trang này hiện đang diễn ra với C # 7.1.NET 4.7 hỗ trợ cú pháp mỏng cũng tạo ra mã IL tốt nhất.

Ví dụ ban đầu của OP ...

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}

trở nên đơn giản ...

if (o is int x)
{
    // ...use x in here
}

Tôi đã thấy rằng một cách sử dụng phổ biến cho cú pháp mới là khi bạn đang viết một loại giá trị .NET (tức là structtrong C # ) thực hiện IEquatable<MyStruct>(như hầu hết nên). Sau khi thực hiện Equals(MyStruct other)phương thức gõ mạnh , giờ đây bạn có thể chuyển hướng một cách duyên dáng phần Equals(Object obj)ghi đè chưa được gõ (được kế thừa từ Object) sang phương thức như sau:

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);

 


Phụ lục: Các Releasebuild IL mã cho hai chức năng ví dụ đầu tiên hiển thị ở trên trong câu trả lời này (tương ứng) được đưa ra ở đây. Mặc dù mã IL cho cú pháp mới thực sự nhỏ hơn 1 byte, nhưng nó chủ yếu thắng lớn bằng cách thực hiện các cuộc gọi bằng không (so với hai) và tránh unboxhoạt động hoàn toàn khi có thể.

// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret

// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret

Để kiểm tra thêm, chứng minh nhận xét của tôi về hiệu suất của cú pháp C # 7 mới vượt qua các tùy chọn có sẵn trước đó, xem tại đây (đặc biệt, ví dụ 'D').


9

Hồ sơ thêm:

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

    static void Main(string[] args)
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int)o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}

Đầu ra:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282

Những gì chúng ta có thể suy ra từ những con số này?

  • Đầu tiên, cách tiếp cận is-then-cast nhanh hơn đáng kể so với cách tiếp cận. 303 so với 3524
  • Thứ hai, .Value chậm hơn một chút so với truyền. 3524 so với 3272
  • Thứ ba, .HasValue chậm hơn một chút so với sử dụng thủ công (tức là sử dụng ). 3524 so với 3282
  • Thứ tư, thực hiện so sánh táo với táo (tức là cả việc gán HasValue mô phỏng và chuyển đổi Giá trị mô phỏng xảy ra cùng nhau) giữa phương pháp mô phỏng như thậtthực , chúng ta có thể thấy mô phỏng vẫn nhanh hơn đáng kể so với thực tế . 395 so với 3524
  • Cuối cùng, dựa trên kết luận thứ nhất và thứ tư, có gì đó không đúng khi thực hiện ^ _ ^

8

Tôi không có thời gian để thử nó, nhưng bạn có thể muốn có:

foreach (object o in values)
        {
            int? x = o as int?;

như

int? x;
foreach (object o in values)
        {
            x = o as int?;

Bạn đang tạo một đối tượng mới mỗi lần, điều này sẽ không giải thích hoàn toàn vấn đề, nhưng có thể đóng góp.


1
Không, tôi đã chạy nó và nó chậm hơn một chút.
Henk Holterman

2
Khai báo một biến ở một nơi khác chỉ ảnh hưởng đáng kể đến mã được tạo khi biến được bắt (tại thời điểm đó nó ảnh hưởng đến ngữ nghĩa thực tế) theo kinh nghiệm của tôi. Lưu ý rằng nó không tạo ra một đối tượng mới trên heap, mặc dù nó chắc chắn tạo ra một thể hiện mới của int?stack bằng cách sử dụng unbox.any. Tôi nghi ngờ đó là vấn đề - tôi đoán là IL thủ công có thể đánh bại cả hai tùy chọn ở đây ... mặc dù cũng có thể JIT được tối ưu hóa để nhận ra trường hợp is / cast và chỉ kiểm tra một lần.
Jon Skeet

Tôi đã nghĩ rằng dàn diễn viên có thể được tối ưu hóa vì nó đã tồn tại quá lâu.
James Black

1
is / cast là một mục tiêu dễ dàng để tối ưu hóa, đó là một thành ngữ phổ biến khó chịu.
Anton Tykhyy

4
Các biến cục bộ được phân bổ trên ngăn xếp khi khung ngăn xếp cho phương thức được tạo, do đó, việc bạn khai báo biến trong phương thức sẽ không có sự khác biệt nào cả. (Trừ khi nó đóng cửa tất nhiên, nhưng đó không phải là trường hợp ở đây.)
Guffa

8

Tôi đã thử cấu trúc kiểm tra loại chính xác

typeof(int) == item.GetType(), hoạt động nhanh như item is intphiên bản và luôn trả về số (nhấn mạnh: ngay cả khi bạn đã viết một Nullable<int>mảng, bạn sẽ cần sử dụng typeof(int)). Bạn cũng cần null != itemkiểm tra thêm ở đây.

Tuy nhiên

typeof(int?) == item.GetType()vẫn nhanh (trái ngược với item is int?), nhưng luôn trả về false.

Cấu trúc typeof trong mắt tôi là cách nhanh nhất để kiểm tra loại chính xác , vì nó sử dụng RuntimeTypeHandle. Vì các loại chính xác trong trường hợp này không khớp với nullable, tôi đoán là, is/asphải thực hiện thêm phép đo nặng ở đây để đảm bảo rằng trên thực tế nó là một thể hiện của loại Nullable.

Và thành thật: bạn is Nullable<xxx> plus HasValuemua gì cho bạn? Không có gì. Bạn luôn có thể chuyển trực tiếp đến loại (giá trị) bên dưới (trong trường hợp này). Bạn có thể nhận được giá trị hoặc "không, không phải là trường hợp của loại bạn đang yêu cầu". Ngay cả khi bạn đã viết (int?)nullvào mảng, kiểm tra kiểu sẽ trả về false.


Thật thú vị ... ý tưởng sử dụng "as" + HasValue (không phải HasValue, lưu ý) là nó chỉ thực hiện kiểm tra loại một lần thay vì hai lần. Đó là thực hiện "kiểm tra và bỏ hộp" trong một bước duy nhất. Cảm giác như nó sẽ nhanh hơn ... nhưng rõ ràng là không. Tôi không chắc ý của bạn là gì trong câu cuối cùng, nhưng không có thứ nào được gọi là hộp int?- nếu bạn đóng một int?giá trị thì nó kết thúc dưới dạng một hộp int hoặc nulltham chiếu.
Jon Skeet

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

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithCast(values);
        FindSumWithAsAndHas(values);
        FindSumWithAsAndIs(values);


        FindSumWithIsThenAs(values);
        FindSumWithIsThenConvert(values);

        FindSumWithLinq(values);



        Console.ReadLine();
    }

    static void FindSumWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int)o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsAndHas(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Has: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }


    static void FindSumWithAsAndIs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Is: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }







    static void FindSumWithIsThenAs(object[] values)
    {
        // Apple-to-apple comparison with Cast routine above.
        // Using the similar steps in Cast routine above,
        // the AS here cannot be slower than Linq.



        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {

            if (o is int)
            {
                int? x = o as int?;
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then As: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithIsThenConvert(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {            
            if (o is int)
            {
                int x = Convert.ToInt32(o);
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Convert: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }



    static void FindSumWithLinq(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = values.OfType<int>().Sum();
        sw.Stop();
        Console.WriteLine("LINQ: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }
}

Đầu ra:

Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811

[EDIT: 2010-06-19]

Lưu ý: Thử nghiệm trước được thực hiện bên trong VS, gỡ lỗi cấu hình, sử dụng VS2009, sử dụng Core i7 (máy phát triển công ty).

Những điều sau đây được thực hiện trên máy của tôi bằng Core 2 Duo, sử dụng VS2010

Inside VS, Configuration: Debug

Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018




Outside VS, Configuration: Debug

Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944




Inside VS, Configuration: Release

Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932




Outside VS, Configuration: Release

Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936

Phiên bản khung nào bạn đang sử dụng, không quan tâm? Các kết quả trên netbook của tôi (sử dụng .NET 4RC) thậm chí còn ấn tượng hơn - các phiên bản sử dụng Như là nhiều tồi tệ hơn kết quả của bạn. Có lẽ họ đã cải thiện nó cho .NET 4 RTM? Tôi vẫn nghĩ rằng nó có thể nhanh hơn ...
Jon Skeet

@Michael: Bạn đang chạy một bản dựng chưa được tối ưu hóa hay đang chạy trong trình gỡ lỗi?
Jon Skeet

@Jon: bản dựng không được tối ưu hóa, theo trình gỡ lỗi
Michael Buen

1
@Michael: Phải - Tôi có xu hướng xem kết quả hoạt động theo trình gỡ lỗi vì phần lớn không liên quan :)
Jon Skeet

@Jon: Nếu theo trình gỡ lỗi, nghĩa là bên trong VS; vâng, điểm chuẩn trước đó đã được thực hiện theo trình gỡ lỗi. Tôi điểm chuẩn một lần nữa, bên trong VS và bên ngoài nó, và được biên dịch dưới dạng gỡ lỗi và biên dịch thành bản phát hành. Kiểm tra chỉnh sửa
Michael Buen
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.