ประหลาดใจกับประสิทธิภาพด้วย“ as” และประเภท nullable


330

ฉันเพิ่งแก้ไขบทที่ 4 ของ C # ในความลึกซึ่งเกี่ยวข้องกับประเภท nullable และฉันเพิ่มส่วนที่เกี่ยวกับการใช้ตัวดำเนินการ "เป็น" ซึ่งช่วยให้คุณเขียน:

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

ฉันคิดว่านี่เป็นระเบียบจริงๆและมันสามารถปรับปรุงประสิทธิภาพมากกว่า C # 1 ที่เทียบเท่าโดยใช้ "คือ" ตามด้วยนักแสดง - หลังจากทั้งหมดด้วยวิธีนี้เราเพียงแค่ต้องขอการตรวจสอบประเภทแบบไดนามิกครั้งเดียวแล้วตรวจสอบค่าง่าย ๆ .

เรื่องนี้ดูเหมือนจะไม่เป็นอย่างนั้น ฉันได้รวมแอปทดสอบตัวอย่างด้านล่างซึ่งโดยทั่วไปจะรวมจำนวนเต็มทั้งหมดภายในอาร์เรย์วัตถุ - แต่อาร์เรย์นั้นมีการอ้างอิง null และการอ้างอิงสตริงจำนวนมากรวมถึงจำนวนเต็มชนิดบรรจุกล่อง มาตรฐานวัดรหัสที่คุณต้องใช้ใน C # 1, รหัสโดยใช้ตัวดำเนินการ "เป็น" และเพียงเพื่อใช้เป็นโซลูชัน LINQ สำหรับความประหลาดใจของฉันรหัส C # 1 นั้นเร็วกว่าถึง 20 เท่าในกรณีนี้และแม้แต่รหัส LINQ (ซึ่งฉันคาดว่าจะช้าลงเนื่องจากผู้วนซ้ำที่เกี่ยวข้อง) เต้นรหัส "เป็น"

การใช้. NET isinstสำหรับประเภท nullable เพียงช้าจริง ๆ หรือไม่ เป็นส่วนเพิ่มเติมunbox.anyที่ทำให้เกิดปัญหาหรือไม่ มีคำอธิบายอื่นสำหรับเรื่องนี้หรือไม่? ในขณะนี้รู้สึกเหมือนว่าฉันจะต้องมีการเตือนการใช้สิ่งนี้ในสถานการณ์ที่อ่อนไหวต่อประสิทธิภาพ ...

ผล:

นำแสดงโดย: 10,000000: 121
เป็น: 10000000: 2211
LINQ: 10,000000: 2143

รหัส:

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
ทำไมไม่ดูรหัส jitted? แม้แต่ดีบักเกอร์ VS ก็สามารถแสดงได้
Anton Tykhyy

2
ฉันแค่อยากรู้อยากเห็นคุณทดสอบกับ CLR 4.0 หรือไม่?
Dirk Vollmar

1
@ แอนตัน: จุดดี จะทำในบางจุด (แม้ว่าจะไม่ได้อยู่ใน VS ในขณะนี้ :) @divo: ใช่และมันแย่กว่ากันทุกรอบ แต่นั่นเป็นรุ่นเบต้าดังนั้นอาจมีรหัสการดีบักจำนวนมากในนั้น
Jon Skeet

1
วันนี้ฉันเรียนรู้ว่าคุณสามารถใช้asกับประเภท nullable น่าสนใจเนื่องจากไม่สามารถใช้กับประเภทค่าอื่นได้ ที่จริงแล้วน่าแปลกใจมากขึ้น
leppie

3
@Lepp มันทำให้รู้สึกดีให้มันไม่ทำงานในประเภทค่า ลองคิดดูasพยายามโยนให้เป็นประเภทและถ้ามันล้มเหลวมันจะคืนค่าว่าง คุณไม่สามารถตั้งค่าประเภทเป็นโมฆะ
Earlz

คำตอบ:


209

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

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

กำลังส่งไปยัง int หรือไม่ ใช้เวลาทำงานมากขึ้น Nullable<int>แทนค่าของจำนวนเต็มชนิดบรรจุกล่องไม่เข้ากันกับรูปแบบของหน่วยความจำ จำเป็นต้องมีการแปลงและรหัสนั้นยุ่งยากเนื่องจากอาจเป็นประเภท enum ชนิดบรรจุกล่อง คอมไพเลอร์ JIT สร้างการเรียกไปยังฟังก์ชันตัวช่วย CLR ชื่อ JIT_Unbox_Nullable เพื่อให้งานเสร็จ นี่คือฟังก์ชั่นวัตถุประสงค์ทั่วไปสำหรับประเภทของค่าใด ๆ มีรหัสจำนวนมากเพื่อตรวจสอบประเภท และค่าจะถูกคัดลอก ประเมินค่าใช้จ่ายได้ยากเนื่องจากรหัสนี้ถูกล็อคไว้ภายใน mscorwks.dll แต่มีคำแนะนำรหัสเครื่องนับร้อย

วิธีการขยาย Linq OfType () ยังใช้ตัวดำเนินการisและการส่ง อย่างไรก็ตามนี่เป็นนักแสดงประเภททั่วไป คอมไพเลอร์ JIT สร้างการเรียกไปยังฟังก์ชันตัวช่วย JIT_Unbox () ที่สามารถทำการแคสต์เป็นประเภทค่าโดยพลการ ฉันไม่มีคำอธิบายที่ดีว่าทำไมมันถึงช้าเท่านักแสดงNullable<int>เนื่องจากการทำงานน้อยควรมีความจำเป็น ฉันสงสัยว่า ngen.exe อาจทำให้เกิดปัญหาได้ที่นี่


16
ตกลงฉันมั่นใจ ฉันเดาว่าฉันเคยคิดว่า "เป็น" ที่อาจแพงเพราะความเป็นไปได้ในการเดินตามลำดับชั้นการสืบทอด - แต่ในกรณีที่เป็นประเภทค่าไม่มีความเป็นไปได้ของลำดับชั้น . ฉันยังคงคิดว่ารหัส JIT สำหรับกรณีที่สามารถ nullable สามารถปรับให้เหมาะสมโดย JIT มากกว่าที่มันเป็น
Jon Skeet

26

ดูเหมือนว่าสำหรับฉันแล้วisinstมันช้ามาก ๆ สำหรับเกมประเภท nullable ในวิธีที่FindSumWithCastฉันเปลี่ยน

if (o is int)

ถึง

if (o is int?)

ซึ่งยังช้าลงอย่างมีนัยสำคัญการดำเนินการ ข้อแตกต่างใน IL ที่ฉันเห็นก็คือ

isinst     [mscorlib]System.Int32

ถูกเปลี่ยนเป็น

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

1
มันเป็นมากกว่านั้น ใน "โยน" กรณีที่isinstจะตามด้วยการทดสอบเป็นโมฆะแล้วเงื่อนไข unbox.anyในกรณี nullable มีการไม่มีเงื่อนไข unbox.any
Jon Skeet

ใช่เปลี่ยนทั้งสองอย่าง isinstและunbox.anyช้าลงสำหรับประเภทที่ไม่สามารถใช้ได้
Dirk Vollmar

@ จอน: คุณสามารถตรวจสอบคำตอบของฉันเป็นเหตุผลที่จำเป็นต้องใช้นักแสดง (ฉันรู้ว่านี่เก่า แต่ฉันเพิ่งค้นพบ q นี้และคิดว่าฉันควรให้ 2c ของฉันในสิ่งที่ฉันรู้เกี่ยวกับ CLR)
โยฮันเนสรูดอล์ฟ

22

แต่เดิมนี้เริ่มต้นจากการเป็น Comment to Hans Passant ของคำตอบที่ยอดเยี่ยม แต่มันยาวเกินไปดังนั้นฉันต้องการเพิ่มสองสามบิตที่นี่:

ขั้นแรกasผู้ประกอบการ C # จะส่งisinstคำสั่ง IL (เช่นisผู้ดำเนินการ) (คำแนะนำที่น่าสนใจอีกอย่างคือปล่อยออกcastclassมาเมื่อคุณทำการส่งโดยตรงและคอมไพเลอร์รู้ว่าการตรวจสอบรันไทม์ไม่สามารถถูก ommited ได้)

นี่คือสิ่งที่isinstทำ ( ECMA 335 Partition III, 4.6 ):

รูปแบบ: isinst typeTok

typeTokเป็นโทเค็นเมตาดาต้า (กtyperef, typedefหรือtypespec) แสดงให้เห็นระดับที่ต้องการ

หากtypeTokเป็นชนิดค่าที่ไม่ใช่ nullable หรือพารามิเตอร์ชนิดทั่วไปมันถูกตีความว่าเป็น“ชนิดบรรจุกล่อง” typeTok

หากtypeTokเป็นประเภทที่Nullable<T>ไม่มีค่าได้จะถูกตีความว่าเป็น“ กล่อง”T

ที่สำคัญที่สุดคือ:

หากประเภทที่เกิดขึ้นจริง (ไม่ใช่ประเภทการติดตามตรวจสอบ) ของobjคือตรวจสอบมอบหมายให้ typeTok ประเภทแล้วisinstประสบความสำเร็จและobj (ตามผล ) จะถูกส่งกลับไม่เปลี่ยนแปลงในขณะที่การตรวจสอบติดตามชนิดที่เป็นtypeTok ซึ่งแตกต่างจากการข่มขู่ (§1.6) และการแปลง (§3.27) isinstไม่เคยเปลี่ยนประเภทของวัตถุจริงและรักษาเอกลักษณ์ของวัตถุ (ดู Partition I)

ดังนั้นนักฆ่าประสิทธิภาพไม่isinstในกรณีนี้ unbox.anyแต่เพิ่มเติม นี่ไม่ชัดเจนจากคำตอบของฮันส์ในขณะที่เขาดูรหัส JITed เท่านั้น โดยทั่วไปคอมไพเลอร์ C # จะปล่อยunbox.anya หลังจาก a isinst T?(แต่จะละเว้นในกรณีที่คุณทำisinst Tเมื่อTเป็นประเภทการอ้างอิง)

ทำไมถึงทำเช่นนั้น? ไม่เคยมีผลกระทบที่จะได้รับที่เห็นได้ชัดคือคุณจะได้รับกลับมาisinst T? T?แต่คำแนะนำเหล่านี้ทั้งหมดช่วยให้คุณมั่นใจได้ว่าคุณมีสิ่ง"boxed T"ที่ไม่สามารถทำไปT?ได้ จะได้รับความจริงT?ที่เรายังคงต้อง unbox ของเรา"boxed T"ไปT?ซึ่งเป็นเหตุผลที่คอมไพเลอร์ปล่อยออกมาหลังจากที่unbox.any isinstหากคุณคิดเกี่ยวกับสิ่งนี้มันสมเหตุสมผลเพราะ "รูปแบบกล่อง" สำหรับT?เป็นเพียง"boxed T"และการสร้างcastclassและisinstดำเนินการ unbox จะไม่สอดคล้องกัน

การสำรองข้อมูลการค้นพบฮันส์มีข้อมูลบางส่วนจากมาตรฐานนี่มันไป:

(ECMA 335 Partition III, 4.33): unbox.any

เมื่อนำไปใช้กับรูปแบบชนิดบรรจุกล่องของชนิดค่าunbox.anyคำสั่งจะแยกค่าที่อยู่ใน obj (ของประเภทO) (มันเทียบเท่ากับunboxตามด้วยldobj.) เมื่อนำไปใช้กับประเภทอ้างอิงunbox.anyคำสั่งมีผลเช่นเดียวกับcastclasstypeTok

(ECMA 335 Partition III, 4.32): unbox

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


ฉันคิดว่าประโยคที่ยกมาล่าสุดอาจมีการพิมพ์ผิด; ไม่ควร“ ... บนกอง ... ” เป็น“ ในกองดำเนินการ ” ดูเหมือนว่าจะไม่มีการแกะกล่องกลับเข้าไปในอินสแตนซ์ฮีป GC ใหม่บางส่วนจะแลกเปลี่ยนปัญหาเดิมสำหรับสิ่งใหม่เกือบจะเหมือนกัน
Glenn Slayden

19

ที่น่าสนใจฉันส่งข้อเสนอแนะเกี่ยวกับการสนับสนุนผู้ให้บริการผ่านdynamicการสั่งซื้อที่ช้าลงสำหรับNullable<T>(คล้ายกับการทดสอบก่อนหน้านี้ ) - ฉันสงสัยว่าด้วยเหตุผลที่คล้ายกันมาก

Nullable<T>ต้องรัก สิ่งที่สนุกอีกอย่างหนึ่งคือแม้ว่าจุด JIT (และลบออก) nullสำหรับ struct ที่ไม่เป็นโมฆะมัน borks สำหรับ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 นั่นเป็นความแตกต่างที่เจ็บปวดจริงๆ จี๊ด
Jon Skeet

ถ้าไม่มีที่ดีอื่น ๆ ได้ออกมาทั้งหมดนี้ก็ทำให้ฉันรวมถึงคำเตือนสำหรับทั้งรหัสเดิมของฉันและนี้ :)
จอนสกีต

ฉันรู้ว่านี่เป็นคำถามเก่า แต่คุณสามารถอธิบายสิ่งที่คุณหมายถึงโดย "จุด JIT (และลบออก) nullสำหรับ struct ที่ไม่เป็นโมฆะ"? คุณหมายถึงว่ามันมาแทนที่nullด้วยค่าเริ่มต้นหรือบางสิ่งบางอย่างในช่วงรันไทม์?
Justin Morgan เมื่อ

2
@Justin - สามารถใช้เมธอดทั่วไปที่รันไทม์พร้อมกับพีชคณิตพารามิเตอร์ทั่วไป ( Tetc) จำนวนเท่าใดก็ได้ ความต้องการสแต็ก ฯลฯ ขึ้นอยู่กับ args (จำนวนของพื้นที่สแต็กสำหรับโลคัล ฯลฯ ) ดังนั้นคุณจะได้รับ JIT หนึ่งค่าสำหรับการเปลี่ยนแปลงเฉพาะที่เกี่ยวข้องกับประเภทค่า อย่างไรก็ตามการอ้างอิงนั้นมีขนาดเท่ากันดังนั้นให้แบ่งปัน JIT ในขณะที่ทำ JIT ต่อมูลค่าประเภทมันสามารถตรวจสอบสถานการณ์ที่ชัดเจนบางอย่างและพยายามที่จะสรรพสามิตรหัสที่ไม่สามารถเข้าถึงได้เนื่องจากสิ่งต่าง ๆ เช่นโมฆะเป็นไปไม่ได้ มันไม่สมบูรณ์แบบโปรดทราบ นอกจากนี้ฉันไม่สนใจ AOT สำหรับข้อมูลข้างต้น
Marc Gravell

การทดสอบ nullable ที่ไม่ จำกัด นั้นยังคงอยู่ที่ 2.5 คำสั่งของขนาดที่ช้ากว่า แต่มีการเพิ่มประสิทธิภาพบางอย่างเกิดขึ้นเมื่อคุณไม่ได้ใช้countตัวแปร การเพิ่มConsole.Write(count.ToString()+" ");หลังจากwatch.Stop();ในทั้งสองกรณีทำให้การทดสอบอื่นช้าลงโดยอยู่ในลำดับความสำคัญ แต่การทดสอบที่ไม่สามารถ จำกัด ได้จะไม่เปลี่ยนแปลง หมายเหตุนอกจากนี้ยังมีการเปลี่ยนแปลงเมื่อคุณทดสอบกรณีที่เมื่อnullผ่านการยืนยันรหัสเดิมไม่ได้ทำการตรวจสอบเป็นโมฆะและเพิ่มขึ้นสำหรับการทดสอบอื่น ๆ Linqpad
Mark Hurd

12

นี่คือผลลัพธ์ของ FindSumWithAsAndHas ด้านบน: ข้อความแสดงแทน

นี่คือผลลัพธ์ของ FindSumWithCast: ข้อความแสดงแทน

ผลการวิจัย:

  • ใช้asมันทดสอบก่อนว่าวัตถุเป็นตัวอย่างของ Int32; ภายใต้ประทุนจะใช้isinst Int32(ซึ่งคล้ายกับรหัสที่เขียนด้วยมือ: ถ้า (o คือ int)) และการใช้asมันยังไม่มีการยกเลิกวัตถุกล่อง และมันก็เป็นนักฆ่าประสิทธิภาพที่แท้จริงที่จะเรียกคุณสมบัติ (มันยังคงเป็นฟังก์ชั่นภายใต้ประทุน), IL_0027

  • ใช้ cast คุณทดสอบก่อนว่าวัตถุคืออะไรint if (o is int); ภายใต้ประทุนที่ใช้งานisinst Int32อยู่ หากเป็นอินสแตนซ์ของ int คุณสามารถยกเลิกการเลือกค่า IL_002D ได้อย่างปลอดภัย

พูดง่าย ๆ นี่คือรหัสหลอกของasวิธีการใช้:

int? x;

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

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

และนี่คือรหัสหลอกของการใช้วิธีการส่ง:

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

ดังนั้นการร่าย ( (int)a[i], ไวยากรณ์ดูเหมือนนักแสดง แต่จริงๆแล้วการแกะกล่อง, การหล่อและการแกะกล่องจะใช้รูปแบบที่เหมือนกันในครั้งต่อไปที่ฉันจะอวดความรู้ด้วยคำศัพท์ที่ถูกต้อง) วิธีนี้จึงเร็วขึ้นจริง ๆ เมื่อวัตถุเป็นเด็ด intสิ่งเดียวกันไม่สามารถพูดได้ว่าใช้asวิธีการ


11

เพื่อที่จะให้คำตอบล่าสุดนี้เป็นสิ่งที่ควรค่าแก่การกล่าวถึงว่าการอภิปรายส่วนใหญ่ในหน้านี้ได้รับการถกเถียงกันในขณะนี้ด้วยC # 7.1และ. NET 4.7ซึ่งสนับสนุนไวยากรณ์ที่บางซึ่งสร้างรหัส IL ที่ดีที่สุด

ตัวอย่างดั้งเดิมของ OP ...

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

กลายเป็น ...

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

ฉันได้พบว่าการใช้งานทั่วไปหนึ่งครั้งสำหรับไวยากรณ์ใหม่คือเมื่อคุณกำลังเขียนประเภทค่า . NET (เช่นstructในC # ) ที่ใช้IEquatable<MyStruct>(ควรจะมากที่สุด) หลังจากใช้Equals(MyStruct other)วิธีการที่พิมพ์อย่างรุนแรงตอนนี้คุณสามารถเปลี่ยนเส้นทางการEquals(Object obj)แทนที่ที่ไม่ได้พิมพ์ (สืบทอดมาObject) ได้อย่างงดงามในตอนนี้ดังนี้:

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

 


ภาคผนวก:Releaseสร้างILสำหรับครั้งแรกที่สองฟังก์ชั่นตัวอย่างข้างต้นแสดงให้เห็นในคำตอบนี้ (ตามลำดับ) จะได้รับที่นี่ ในขณะที่รหัส IL สำหรับซินแท็กซ์ใหม่นั้นมีขนาดเล็กกว่า 1 ไบต์ แต่ส่วนใหญ่จะชนะได้มากด้วยการโทรเป็นศูนย์ (เทียบกับสอง) และหลีกเลี่ยงการunboxดำเนินการทั้งหมดเมื่อเป็นไปได้

// 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

สำหรับการทดสอบเพิ่มเติมซึ่งยืนยันถึงคำพูดของฉันเกี่ยวกับประสิทธิภาพของไวยากรณ์C # 7ใหม่ที่เกินตัวเลือกที่มีอยู่ก่อนหน้านี้ดูที่นี่ (โดยเฉพาะอย่างยิ่งตัวอย่าง 'D')


9

การสร้างโปรไฟล์เพิ่มเติม:

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);
    }

}

เอาท์พุท:

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

เราสามารถสรุปอะไรจากตัวเลขเหล่านี้

  • ครั้งแรกเป็นแล้วโยนวิธีการอย่างมีนัยสำคัญได้เร็วกว่าเป็นวิธีการ 303 กับ 3524
  • ประการที่สอง. มูลค่าเล็กน้อยช้ากว่าการคัดเลือกนักแสดง 3524 กับ 3272
  • ประการที่สามคือ .HasValue เล็กน้อยช้ากว่าการใช้คู่มือมี (เช่นใช้เป็น ) 3524 กับ 3282
  • ประการที่สี่การทำเปรียบเทียบแอปเปิ้ลไปแอปเปิ้ล (คือทั้งการกำหนดของจำลอง HasValue และการแปลงจำลองราคาที่เกิดขึ้นพร้อมกัน) ระหว่างจำลองเป็นและจริงเป็นวิธีการที่เราสามารถมองเห็นการจำลองเป็นยังคงเป็นอย่างเร็วกว่าจริง 395 กับ 3524
  • สุดท้ายขึ้นอยู่กับข้อสรุปที่แรกและที่สี่มีอะไรบางอย่างผิดปกติกับฐานะ การดำเนินงาน ^ _ ^

8

ฉันไม่มีเวลาลอง แต่คุณอาจต้องการ:

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

เช่น

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

คุณกำลังสร้างวัตถุใหม่ในแต่ละครั้งซึ่งจะไม่อธิบายปัญหาอย่างสมบูรณ์ แต่อาจมีส่วนร่วม


1
ไม่ฉันวิ่งไปนั้นและมันช้าลงเล็กน้อย
Henk Holterman

2
การประกาศตัวแปรในที่ต่าง ๆ มีผลกับโค้ดที่สร้างขึ้นอย่างมีนัยสำคัญเมื่อจับตัวแปรเท่านั้น (ณ จุดนั้นจะมีผลต่อความหมายที่แท้จริง) ในประสบการณ์ของฉัน โปรดทราบว่ามันไม่ได้สร้างวัตถุใหม่บนฮีปแม้ว่ามันจะสร้างอินสแตนซ์ใหม่ของint?การใช้สแต็กunbox.anyอย่างแน่นอน ฉันสงสัยว่าเป็นปัญหา - ฉันเดาว่า IL ประดิษฐ์ขึ้นมาด้วยมือสามารถเอาชนะตัวเลือกทั้งสองได้ที่นี่ ... ถึงแม้ว่ามันอาจเป็นไปได้ว่า JIT นั้นได้รับการปรับให้เหมาะสำหรับกรณีที่เป็นแบบหล่อและตรวจสอบเพียงครั้งเดียว
Jon Skeet

ฉันคิดว่านักแสดงอาจจะได้รับการปรับให้เหมาะสมเพราะมันใช้งานได้นานมาก
James Black

1
is / cast เป็นเป้าหมายที่ง่ายสำหรับการปรับให้เหมาะสมมันเป็นสำนวนสามัญที่น่ารำคาญ
Anton Tykhyy

4
ตัวแปรโลคัลถูกจัดสรรบนสแต็กเมื่อสร้างเฟรมสแต็กสำหรับเมธอดดังนั้นคุณจึงประกาศว่าตัวแปรในเมธอดไม่สร้างความแตกต่างเลย (เว้นแต่จะปิดแน่นอน แต่ไม่ใช่ในกรณีนี้)
Guffa

8

ฉันลองสร้างชนิดตรวจสอบที่แน่นอน

typeof(int) == item.GetType()ซึ่งทำงานเร็วเท่ากับitem is intเวอร์ชันและส่งคืนตัวเลขเสมอ (การเน้น: แม้ว่าคุณจะเขียน a Nullable<int>ไปยังอาร์เรย์คุณจะต้องใช้typeof(int)) คุณต้องnull != itemตรวจสอบเพิ่มเติมที่นี่

อย่างไรก็ตาม

typeof(int?) == item.GetType()อยู่อย่างรวดเร็ว (ตรงกันข้ามกับitem is int?) แต่จะส่งกลับค่าเท็จเสมอ

typeof-construct อยู่ในสายตาของฉันเป็นวิธีที่เร็วที่สุดสำหรับการตรวจสอบประเภทที่แน่นอนเนื่องจากใช้ RuntimeTypeHandle เนื่องจากประเภทที่แน่นอนในกรณีนี้ไม่ตรงกับ nullable ฉันเดาคือis/asต้องทำการเพิ่มน้ำหนักที่นี่เพื่อให้แน่ใจว่าในความเป็นจริงแล้วเป็นตัวอย่างของประเภทที่ใช้ค่า Nullable

และอย่างซื่อสัตย์: คุณis Nullable<xxx> plus HasValueซื้ออะไรคุณ ไม่มีอะไร คุณสามารถไปที่ประเภทพื้นฐาน (ค่า) เสมอ (ในกรณีนี้) คุณได้รับค่าหรือ "ไม่ไม่ใช่ตัวอย่างที่คุณขอ" แม้ว่าคุณเขียน(int?)nullไปยังอาร์เรย์การตรวจสอบชนิดจะส่งคืนเท็จ


ที่น่าสนใจ ... คิดของการใช้ที่ "เป็น" + HasValue (ไม่ได้เป็นบวก HasValue ทราบ) ก็คือว่ามันเป็นเพียงการดำเนินการตรวจสอบชนิดหนึ่งครั้งแทนที่จะเป็นครั้งที่สอง กำลังดำเนินการ "ตรวจสอบและเลิกทำ" ในขั้นตอนเดียว มันให้ความรู้สึกว่ามันควรจะเร็วกว่า ... ฉันไม่แน่ใจว่าคุณหมายถึงประโยคสุดท้าย แต่ไม่มีอะไรบรรจุอยู่ในกล่องint?ถ้าคุณทำกล่องint?ค่ามันจะกลายเป็น int แบบกล่องหรือnullอ้างอิง
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);
    }
}

ขาออก:

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

[แก้ไข: 2010-06-19]

หมายเหตุ: การทดสอบก่อนหน้านี้ทำใน VS, ดีบักการกำหนดค่า, ใช้ VS2009, ใช้ Core i7 (เครื่องพัฒนา บริษัท )

ต่อไปนี้ทำบนเครื่องของฉันโดยใช้ Core 2 Duo โดยใช้ 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

คุณใช้เฟรมเวิร์กเวอร์ชันใดด้วยความสนใจ? ผลใน netbook ของฉัน (ใช้ .NET 4RC) เป็นอย่างมากแม้ - รุ่นใช้ในฐานะที่เป็นมากยิ่งกว่าผลลัพธ์ของคุณ พวกเขาอาจปรับปรุงมันสำหรับ. NET 4 RTM? ฉันยังคิดว่ามันน่าจะเร็วกว่านี้ ...
Jon Skeet

@Michael: คุณเรียกใช้งานบิลด์ที่ไม่มีการกำหนดเวลาหรือทำงานในดีบักเกอร์หรือไม่?
Jon Skeet

@ จอน: งานสร้างที่ไม่ได้เพิ่มประสิทธิภาพภายใต้ดีบักเกอร์
Michael Buen

1
@Michael: ขวา - ฉันมักจะดูผลการปฏิบัติงานภายใต้การดีบักเกอร์เป็นที่ไม่เกี่ยวข้องส่วนใหญ่ :)
จอน Skeet

@ จอน: หากภายใต้ดีบักเกอร์ความหมายภายในของ VS; ใช่เกณฑ์มาตรฐานก่อนหน้านี้ทำภายใต้ดีบักเกอร์ ฉันเปรียบเทียบมาตรฐานอีกครั้งภายใน VS และภายนอกและรวบรวมเป็น debug และคอมไพล์เป็นรีลีส ตรวจสอบการแก้ไข
Michael Buen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.