บทความเกี่ยวกับการเร่งการสะท้อนที่ไม่สำเร็จ

ฉันจะอธิบายชื่อบทความทันที แผนเดิมคือการให้คำแนะนำที่ดีและเชื่อถือได้เกี่ยวกับวิธีการเร่งการใช้การสะท้อนโดยใช้ตัวอย่างง่ายๆ แต่สมจริง แต่ในระหว่างการเปรียบเทียบ ปรากฏว่าการสะท้อนไม่ได้ช้าอย่างที่ฉันคิด LINQ นั้นช้ากว่าในฝันร้ายของฉัน แต่สุดท้ายกลับกลายเป็นว่าวัดขนาดผิดด้วย... รายละเอียดของเรื่องราวชีวิตนี้อยู่ระหว่างการตัดและในคอมเมนต์ครับ เนื่องจากตัวอย่างนี้ค่อนข้างเป็นเรื่องธรรมดาและนำไปใช้ตามหลักการตามปกติในองค์กรจึงกลายเป็นสิ่งที่น่าสนใจสำหรับฉันในการสาธิตชีวิต: ผลกระทบต่อความเร็วของหัวข้อหลักของบทความคือ ไม่สังเกตเห็นได้เนื่องจากตรรกะภายนอก: Moq, Autofac, EF Core และ "สายรัด" อื่นๆ

ฉันเริ่มทำงานภายใต้ความประทับใจของบทความนี้: เหตุใดการสะท้อนกลับจึงช้า

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

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

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

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

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

การสะท้อนการเรียกบังคับให้ CLR ผ่านแอสเซมบลีเพื่อค้นหาสิ่งที่ต้องการ ดึงข้อมูลเมตาขึ้นมา แยกวิเคราะห์ ฯลฯ นอกจากนี้ การสะท้อนกลับในขณะที่ลำดับการเคลื่อนที่นำไปสู่การจัดสรรหน่วยความจำจำนวนมาก เราใช้หน่วยความจำจนหมดแล้ว CLR เปิดโปง GC และรอยสลักเริ่มต้นขึ้น มันควรจะช้าอย่างเห็นได้ชัดเชื่อฉัน หน่วยความจำจำนวนมหาศาลบนเซิร์ฟเวอร์ที่ใช้งานจริงหรือเครื่องคลาวด์สมัยใหม่ไม่ได้ป้องกันความล่าช้าในการประมวลผลในระดับสูง ในความเป็นจริง ยิ่งมีหน่วยความจำมากเท่าไร คุณก็จะยิ่งสังเกตเห็นวิธีการทำงานของ GC มากขึ้นเท่านั้น ตามทฤษฎีแล้ว การสะท้อนกลับเป็นผ้าขี้ริ้วสีแดงพิเศษสำหรับเขา

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

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

สิ่งนี้สำเร็จได้อย่างไร? โดยพื้นฐานแล้ว สิ่งนี้ไม่แตกต่างจากตรรกะที่แพลตฟอร์มใช้ในการสร้างโค้ด JIT เมื่อเรียกใช้เมธอดเป็นครั้งแรก วิธีการนั้นจะถูกคอมไพล์ (และใช่ กระบวนการนี้ไม่เร็ว) ในการเรียกครั้งต่อๆ ไป การควบคุมจะถูกถ่ายโอนไปยังเมธอดที่คอมไพล์แล้ว และจะไม่มีการดึงประสิทธิภาพลงอย่างมีนัยสำคัญ

ในกรณีของเรา คุณยังสามารถใช้การคอมไพล์ JIT แล้วใช้พฤติกรรมการคอมไพล์ที่มีประสิทธิภาพเช่นเดียวกับ AOT นิพจน์จะมาช่วยเราในกรณีนี้

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

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

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

ตอนนี้เกี่ยวกับรหัส ลองดูตัวอย่างที่อิงจากความเจ็บปวดล่าสุดของฉันที่ฉันต้องเผชิญในการผลิตที่จริงจังของสถาบันสินเชื่อที่จริงจัง เอนทิตีทั้งหมดเป็นเรื่องสมมติดังนั้นจึงไม่มีใครเดาได้

มีสาระสำคัญบางอย่าง ให้มีการติดต่อ. มีตัวอักษรที่มีเนื้อหามาตรฐานซึ่งตัวแยกวิเคราะห์และไฮเดรเตอร์สร้างผู้ติดต่อแบบเดียวกันนี้ มีจดหมายมาถึง เราอ่าน แยกวิเคราะห์เป็นคู่คีย์-ค่า สร้างผู้ติดต่อ และบันทึกไว้ในฐานข้อมูล

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

เราดำเนินการสร้างการทดสอบ ได้ผล

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

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

“เร็ว” (คำนำหน้าเร็วในการวัดประสิทธิภาพ):

 protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var setterMapItem in _proprtySettersMap)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == setterMapItem.Key);
                setterMapItem.Value(contact, correlation?.Value);
            }
            return contact;
        }

ดังที่เราเห็นแล้วว่ามีการใช้คอลเลกชันแบบคงที่ที่มีคุณสมบัติตัวตั้งค่า - แลมบ์ดาที่คอมไพล์แล้วซึ่งเรียกเอนทิตีตัวตั้งค่า สร้างโดยรหัสต่อไปนี้:

        static FastContactHydrator()
        {
            var type = typeof(Contact);
            foreach (var property in type.GetProperties())
            {
                _proprtySettersMap[property.Name] = GetSetterAction(property);
            }
        }

        private static Action<Contact, string> GetSetterAction(PropertyInfo property)
        {
            var setterInfo = property.GetSetMethod();
            var paramValueOriginal = Expression.Parameter(property.PropertyType, "value");
            var paramEntity = Expression.Parameter(typeof(Contact), "entity");
            var setterExp = Expression.Call(paramEntity, setterInfo, paramValueOriginal).Reduce();
            
            var lambda = (Expression<Action<Contact, string>>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

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

“ช้า” (คำนำหน้าช้าในการวัดประสิทธิภาพ):

        protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var property in _properties)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == property.Name);
                if (correlation?.Value == null)
                    continue;

                property.SetValue(contact, correlation.Value);
            }
            return contact;
        }

ที่นี่เราจะข้ามคุณสมบัติทันทีและเรียก SetValue โดยตรง

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

ตอนนี้เรามาดู BenchmarkDotNet และตรวจสอบประสิทธิภาพกัน แล้วจู่ๆ... (สปอยล์ - ผลนี้ไม่ถูกต้อง รายละเอียดอยู่ด้านล่าง)

บทความเกี่ยวกับการเร่งการสะท้อนที่ไม่สำเร็จ

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

ให้กับคุณ

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

นี่คือผลลัพธ์ของการทดสอบซ้ำ:

บทความเกี่ยวกับการเร่งการสะท้อนที่ไม่สำเร็จ

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

รหัสมาตรฐานมีอยู่ที่นี่ ใครๆ ก็สามารถตรวจสอบคำพูดของฉันได้อีกครั้ง:
การทดสอบการสะท้อนกลับของฮาบรา

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

PPS: ขอบคุณผู้ใช้ มิทรี ทิโคนอฟ @0x1000000 สำหรับการค้นพบข้อผิดพลาดของฉันในการตั้งค่า Moq ซึ่งส่งผลต่อการวัดครั้งแรก หากผู้อ่านท่านใดมีกรรมพอโปรดกดไลค์ด้วย ชายคนนั้นหยุด ชายคนนั้นอ่าน ชายคนนั้นตรวจสอบอีกครั้ง และชี้ให้เห็นข้อผิดพลาด ฉันคิดว่านี่ควรค่าแก่การเคารพและเห็นใจ

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

ที่มา: will.com

เพิ่มความคิดเห็น