Oktatóanyag: Összetett hozzárendelési operátorok létrehozása

A C#14 felhasználó által definiált összetett hozzárendelési operátorokat ad hozzá, amelyek lehetővé teszik az adatstruktúra mutációját új példány létrehozása helyett. A C#korábbi verzióiban a kifejezés:

a += b;

A következő kódra kibővítve lett:

// compiler-generated code prior to C# 13:
var tmp = a + b;
a = tmp;

A bővítés a a típustól függően túlzott foglalásokhoz vezet új példányok létrehozásához, vagy több tulajdonság értékeinek másolásához, hogy értékeket állítson be a másolaton. Ha felhasználó által definiált operátort += ad hozzá, az azt jelzi, hogy egy típus jobb munkát végezhet a célobjektum helyben történő frissítésével.

A C# támogatja a meglévő bővítést, de csak akkor használja, ha egy összetett, felhasználó által definiált operátor nem érhető el.

Ebben az útmutatóban Ön:

  • Futtassa a kezdő mintát.
  • A kód szűk keresztmetszeteinek azonosítása.
  • Új összetett hozzárendelési operátorok implementálása.
  • Elemezze a kész mintát.

Előfeltételek

A kiindulási minta elemzése

Futtassa a kezdőalkalmazást. A GitHub-adattárból dotnet/docs szerezheti be. A mintaalkalmazás egy színházi helyszínen szimulálja a koncertek részvételének nyomon követését. A szimuláció a valósághű érkezési mintákat modellezi egész este, a korai résztvevőktől a fő rohanásig a showtime előtt. Ez a szimuláció bemutatja az objektumlefoglalásokat, amikor hagyományos operátorokat használ, szemben a felhasználó által definiált összetett hozzárendelési operátorokkal lehetséges hatékonyságnövekedéssel.

Az alkalmazás több színházi kapun (főszinten és erkélyszakaszon) keresztül követi nyomon a részvételt a koncertlátogatók érkezésekor. Minden kapu egy rekord használatával GateAttendance tartja nyilván a résztvevők számát. A szimuláció során a kód gyakran frissíti ezeket a számokat növekményes (++) és összeadási (+=) műveletekkel. A következő kód a szimuláció egy részét mutatja be:

// Gate 1 - busiest entrance (target: ~100-130 people)
gates.MainFloorGates[0] += random.Next(8, 15);     // Corporate group
++gates.MainFloorGates[0];                          // Single patron
gates.MainFloorGates[0] += random.Next(20, 30);    // Tour/large group arrival
gates.MainFloorGates[0] += random.Next(5, 12);     // Family groups
++gates.MainFloorGates[0];                          // Solo attendee

// Gate 2 - second busiest (target: ~85-115 people)
gates.MainFloorGates[1] = gates.MainFloorGates[1] + random.Next(6, 12);  // Group booking
++gates.MainFloorGates[1];                          // Single patron
gates.MainFloorGates[1] += random.Next(18, 28);    // Large family/reunion
gates.MainFloorGates[1] += random.Next(8, 15);     // Corporate/business group
gates.MainFloorGates[1] += random.Next(4, 8);      // Couples/small groups
++gates.MainFloorGates[1];                          // Individual patron

A kód szűk keresztmetszeteinek azonosítása

A hagyományos operátorok esetében minden művelet új GateAttendance példányt hoz létre, ami jelentős memórialefoglalásokhoz vezet. Az indító GateAttendancenem módosítható. A kód nem tudja módosítani az objektum állapotát az inicializálás után. A tervezési döntéshez objektumok másolása szükséges, ha módosítania kell az állapotot.

A szimuláció futtatásakor a részletes kimenet a következőt mutatja:

  • Kapunkénti jelenléti számok különböző érkezési időszakokban.
  • Teljes jelenléti követés az összes kapun.
  • Egy végleges átfogó jelentés a részvételi statisztikákkal.

Az alábbi szöveg néhány példakimenetet jelenít meg:

Peak arrival time - all gates busy...

Peak rush period completed - all gates processed heavy traffic.

--- Gate Status After Main Rush (7:15 PM) ---
Main Floor Gates:
  Main-Floor-Gate-1: 145 attendees
  Main-Floor-Gate-2: 168 attendees
  Main-Floor-Gate-3: 149 attendees
  Main-Floor-Gate-4:  71 attendees
  Main Floor Subtotal: 533 attendees

Balcony Gates:
  Balcony-Gate-Left: 164 attendees
  Balcony-Gate-Right: 134 attendees
  Balcony Subtotal: 298 attendees

Total Current Attendance: 831 / 1000

--- Late Arrivals (7:15 PM - 7:30 PM) ---
Final patrons arriving before curtain...

Final arrivals processed - concert about to begin!

Vizsgálja meg az indító GateAttendance rekord osztályt:

public record class GateAttendance(string GateId)
{
    public int Count { get; init; }

    public static GateAttendance operator ++(GateAttendance gate)
    {
        GateAttendance updateGate = gate with { Count = gate.Count + 1 };
        return updateGate;
    }

    public static GateAttendance operator +(GateAttendance gate, int partySize)
    {
        GateAttendance updateGate = gate with { Count = gate.Count + partySize };
        return updateGate;
    }
}

A InitialImplementation.GateAttendance rekord a C#-ban történő operátor-túlterhelés hagyományos megközelítését mutatja be. Figyelje meg, hogy a növekmény operátor (++) és az összeadási operátor (+) is teljesen új GateAttendance példányokat hoz létre a with kifejezéssel. Minden alkalommal, amikor gate++ vagy gate += partySize kiírásra kerül, az operátorok egy új rekordpéldányt hoznak létre a frissített Count értékkel, majd visszaadják az új példányt. Bár ez a megközelítés fenntartja a nem módosíthatóságot és a szálbiztonságot, ez a gyakori memóriafoglalások költségével jár. A sok művelettel rendelkező forgatókönyvekben – például a több száz nézői frissítéssel rendelkező koncertszimulációhoz hasonlóan – ezek a foglalások gyorsan halmozódnak fel, ami hatással lehet a teljesítményre és növeli a szemétgyűjtési nyomást.

Ennek a foglalási viselkedésnek a méréséhez próbálja meg futtatni a .NET objektumfoglalás-követő eszközt a Visual Studióban. Amikor a koncertszimuláció során profilozza az aktuális megvalósítást, felfedezheti, hogy 134 GateAttendance objektumot foglal le a viszonylag kis méretű szimuláció elvégzéséhez. Minden operátorhívás létrehoz egy új példányt, amely bemutatja, hogy a foglalások milyen gyorsan halmozódhatnak fel valós forgatókönyvekben. Ez a mérés konkrét alapkonfigurációt biztosít az összetett hozzárendelési operátorokkal elért teljesítménybeli fejlesztések összehasonlításához.

Összetett hozzárendelési operátorok implementálása

A C# 14 olyan felhasználó által definiált összetett hozzárendelési operátorokat vezet be, amelyek új példányok létrehozása helyett helyszíni mutációkat tesznek lehetővé. Ezek az operátorok hatékonyabb alternatívát nyújtanak a hagyományos mintával, miközben fenntartják a jól ismert összetett hozzárendelési szintaxist.

Az összetett hozzárendelési operátorok olyan új szintaxist használnak, amely a void kulcsszó alkalmazásával deklarál visszatérési operator metódusokat. Adja hozzá a következő operátorokat az GateAttendance osztályhoz:

public void operator +=(int value) => this.property += value;
public void operator ++() => this.property++;

A hagyományos operátorok fő különbségei a következők:

  • Mutáció: Az aktuális példányt közvetlenül this használatával módosítják.
  • Nincsenek új példányok: Az új objektumokat visszaadó hagyományos operátorokkal ellentétben az összetett operátorok módosítják a meglévőket.
  • Visszatérési típus: Az összetett hozzárendelési operátorok nem a típust magát, hanem void adják vissza.

Amikor a fordító összetett hozzárendelési kifejezésekkel (például a += b vagy ++a) találkozik, az a következő feloldási sorrendet követi:

  1. Összetett hozzárendelési operátor keresése: Ha a típus felhasználó által definiált összetett hozzárendelési operátort határoz meg (például), +=++használja közvetlenül.
  2. Visszalépés a hagyományos expanzióra: Ha nincs összetett operátor, bontsa ki a hagyományos formát (a = a + b).

Ez azt jelenti, hogy mindkét megközelítés egyszerre implementálható. Az összetett operátorok elsőbbséget élveznek, ha elérhetők, de a hagyományos operátorok tartalékként szolgálnak olyan helyzetekben, ahol az összetett hozzárendelés nem megfelelő.

Az összetett hozzárendelési operátorok számos előnnyel járnak:

  • Csökkentett foglalások: Módosítsa az objektumokat helyben, ahelyett, hogy új példányokat hozna létre.
  • Jobb teljesítmény: Az ideiglenes objektumok létrehozásának megszüntetése és a szemétgyűjtési nyomás csökkentése.
  • Ismerős szintaxis: Használja ugyanazt +=a szintaxist, ++ amelyet a fejlesztők már ismernek.
  • Visszamenőleges kompatibilitás: A hagyományos operátorok továbbra is tartalékként működnek.

Az új összetett hozzárendelési operátorok a következő kódban jelennek meg:

public void operator ++() => Count++;

public void operator +=(int partySize) => Count += partySize;

Megjegyzés:

A C++-t ismerő fejlesztőkben felmerülhet a kérdés, hogy miért van szükség csak egy ++ vagy -- operátorra. A fordító létrehozza a kódot a módosítás előtti vagy utáni kifejezés visszatérési értékként való használatára. A fordító által létrehozott kód az eredeti vagy a módosított érték alapján végzi el a hozzárendelést, attól függően, hogy az előnövekmény (++x) vagy a növekmény utáni (x++) meghívása megtörtént-e.

Kész minta elemzése

Most, hogy implementálta az összetett hozzárendelési operátorokat, ideje felmérni a teljesítmény javulását. A memóriafoglalások drámai különbségének méréséhez futtassa újra a .NET objektumfoglalás-követő eszközt a frissített kódon.

Ha az alkalmazást az összetett hozzárendelési operátorokkal profilozza, figyelemre méltó csökkenést tapasztal: a teljes koncertszimuláció során csak 10 GateAttendance objektum van lefoglalva az előző 134 foglaláshoz képest. Ez a frissítés 92% csökkenti az objektumlefoglalásokat!

A fennmaradó 10 kiosztás az egyes színházi kapuk GateAttendance kezdeti példányainak létrehozásából származik (négy fő emeleti kapu + két erkélykapu = hat kezdeti példány), valamint néhány további kiosztás a szimuláció más részeiből, amelyek nem használják az összetett operátorokat.

Ez az allokációcsökkentés valós teljesítménybeli előnyöket eredményez.

  • Csökkent memóriaterhelés: Ritkábban előforduló szemétgyűjtési ciklusok.
  • Jobb gyorsítótár-hely: Kevesebb objektumlétrehozás kevesebb memóriatöredezettséget jelent.
  • Továbbfejlesztett átviteli teljesítmény: a kiosztásból és a gyűjtési többletterhelésből mentett processzorciklusok.
  • Méretezhetőség: Az előnyök megsokszorozódnak a nagyobb műveleti kötettel rendelkező forgatókönyvekben.

A teljesítmény javítása még jelentősebbé válik az éles alkalmazásokban, ahol hasonló minták sokkal nagyobb méretekben fordulnak elő – képzelje el, hogy több millió tranzakciót követ nyomon, több ezer számlálót frissít, vagy nagy gyakoriságú adatfolyamokat dolgoz fel.

Próbáljon meg más lehetőségeket azonosítani az összetett hozzárendelési operátorok számára a kódbázisban. Keresd meg azokat a mintákat, amelyekben hagyományos hozzárendelési műveleteket használsz, és fontold meg, hogy hasznos lehet-e az összetett hozzárendelés szintaxisa. Miközben ezen műveletek némelyike már használja a += szimulációs kódban, az alapelv minden olyan forgatókönyvre vonatkozik, ahol inkább az objektumok ismételt módosítása történik, mint új példányok létrehozása.

Utolsó kísérletként módosítsa a GateAttendance típust a record class-ről record struct-re. Ez egy másik optimalizálás, és ebben a szimulációban működik, mert a szerkezet kis memóriaigényű. A szerkezet másolása GateAttendance nem költséges művelet. Ennek ellenére kisebb javításokat érhetsz el.