Udostępnij za pośrednictwem


Porady i wskazówki dotyczące wydajności w aplikacjach .NET

 

Emmanuel Schanzer
Microsoft Corporation

Sierpień 2001 r.

Krótki opis: Ten artykuł jest przeznaczony dla deweloperów, którzy chcą dostosować swoje aplikacje w celu uzyskania optymalnej wydajności w świecie zarządzanym. Przykładowy kod, wyjaśnienia i wytyczne dotyczące projektowania są przeznaczone dla aplikacji Database, Windows Forms i ASP, a także porady specyficzne dla języka dla programu Microsoft Visual Basic i zarządzanego języka C++. (25 drukowanych stron)

Zawartość

Omówienie
Porady dotyczące wydajności dla wszystkich aplikacji
Porady dotyczące dostępu do bazy danych
Porady dotyczące wydajności aplikacji ASP.NET
Porady dotyczące przenoszenia i programowania w Visual Basic
Porady dotyczące przenoszenia i programowania w zarządzanym języku C++
Dodatkowe zasoby
Dodatek: Koszt wywołań wirtualnych i alokacji

Omówienie

Ten oficjalny dokument jest przeznaczony dla deweloperów piszących aplikacje dla platformy .NET i szukając różnych sposobów poprawy wydajności. Jeśli jesteś deweloperem, który jest nowym użytkownikiem platformy .NET, musisz zapoznać się zarówno z platformą, jak i z wybranym językiem. Ten dokument ściśle opiera się na tej wiedzy i zakłada, że programista wie już wystarczająco dużo, aby program działał. Jeśli przenosisz istniejącą aplikację na platformę .NET, warto przeczytać ten dokument przed rozpoczęciem portu. Niektóre wskazówki są tutaj pomocne w fazie projektowania i zawierają informacje, o których należy wiedzieć przed rozpoczęciem portu.

Ten dokument jest podzielony na segmenty z poradami zorganizowanymi według projektu i typu dewelopera. Pierwszy zestaw wskazówek to musi być odczytywany do pisania w dowolnym języku i zawiera porady, które pomogą Ci w każdym języku docelowym w środowisku uruchomieniowym języka wspólnego (CLR). Powiązana sekcja jest zgodna z poradami specyficznymi dla platformy ASP. Drugi zestaw porad jest zorganizowany według języka, który zajmuje się konkretnymi poradami dotyczącymi korzystania z zarządzanych języków C++ i Microsoft® Visual Basic®.

Ze względu na ograniczenia harmonogramu czas wykonywania wersji 1 (wersja 1) musiał najpierw dotyczyć najszerszej funkcjonalności, a następnie radzić sobie z optymalizacjami specjalnych przypadków później. Powoduje to kilka przypadków gołębi, w których wydajność staje się problemem. W związku z tym ten dokument zawiera kilka wskazówek, które zostały zaprojektowane w celu uniknięcia tego przypadku. Te porady nie będą istotne w następnej wersji (vNext), ponieważ te przypadki są systematycznie identyfikowane i optymalizowane. Wskażę je, gdy idziemy, i to do ciebie należy zdecydować, czy jest to warte wysiłku.

Porady dotyczące wydajności dla wszystkich aplikacji

Istnieje kilka wskazówek do zapamiętania podczas pracy nad clR w dowolnym języku. Są one istotne dla wszystkich i powinny być pierwszą linią obrony w przypadku problemów z wydajnością.

Zgłaszanie mniejszej liczby wyjątków

Zgłaszanie wyjątków może być bardzo kosztowne, dlatego upewnij się, że nie zgłaszasz ich zbyt wiele. Użyj narzędzia Perfmon, aby zobaczyć, ile wyjątków zgłasza aplikacja. Może być zaskoczeniem, że niektóre obszary aplikacji zgłaszają więcej wyjątków niż oczekiwano. Aby uzyskać lepszy stopień szczegółowości, można również programowo sprawdzić liczbę wyjątków przy użyciu liczników wydajności.

Znalezienie i projektowanie kodu o dużym wyjątku może spowodować przyzwoitą wygraną wydajności. Należy pamiętać, że nie ma to nic wspólnego z blokami try/catch: koszt jest naliczany tylko wtedy, gdy jest zgłaszany rzeczywisty wyjątek. Możesz użyć jak najwięcej bloków try/catch. Korzystanie z wyjątków bezinteresownie polega na utracie wydajności. Na przykład należy trzymać się z dala od takich rzeczy, jak używanie wyjątków dla przepływu sterowania.

Oto prosty przykład tego, jak kosztowne mogą być wyjątki: po prostu uruchomimy pętlę For , generując tysiące lub wyjątki, a następnie kończąc. Spróbuj skomentować instrukcję throw, aby zobaczyć różnicę szybkości: te wyjątki powodują ogromne obciążenie.

public static void Main(string[] args){
  int j = 0;
  for(int i = 0; i < 10000; i++){
    try{   
      j = i;
      throw new System.Exception();
    } catch {}
  }
  System.Console.Write(j);
  return;   
}
  • Uważaj! Czas wykonywania może zgłaszać wyjątki samodzielnie! Na przykład Response.Redirect() zgłasza wyjątek ThreadAbort . Nawet jeśli nie zgłaszasz jawnie wyjątków, możesz użyć funkcji, które to robią. Upewnij się, że sprawdzasz narzędzie Perfmon, aby uzyskać rzeczywistą historię, oraz debuger w celu sprawdzenia źródła.
  • Deweloperzy języka Visual Basic: język Visual Basic domyślnie włącza sprawdzanie nietęt, aby upewnić się, że takie elementy jak przepełnienie i dzielenie przez zero zgłaszają wyjątki. Możesz wyłączyć tę opcję, aby zwiększyć wydajność.
  • Jeśli używasz modelu COM, należy pamiętać, że hrESULTS może zwracać jako wyjątki. Upewnij się, że uważnie śledzisz te czynności.

Wykonaj masywne wywołania

Wywołanie fragmentów to wywołanie funkcji, które wykonuje kilka zadań, takich jak metoda, która inicjuje kilka pól obiektu. Ma to być wyświetlane w przypadku wywołań czatty, które wykonują bardzo proste zadania i wymagają wielu wywołań w celu wykonania czynności (takich jak ustawienie każdego pola obiektu przy użyciu innego wywołania). Ważne jest, aby wykonać fragmenty, a nie wywołania czatty między metodami, w których obciążenie jest wyższe niż w przypadku prostych wywołań metody intra-AppDomain. Wywołania P/Invoke, międzyoperajności i komunikacji telefonicznej wszystkie wywołania przenoszą na siebie i chcesz używać ich oszczędnie. W każdym z tych przypadków należy spróbować zaprojektować aplikację tak, aby nie polegała na małych, częstych wywołaniach, które przenoszą tyle narzutów.

Przejście odbywa się za każdym razem, gdy kod zarządzany jest wywoływany z niezarządzanego kodu i na odwrót. Czas wykonywania sprawia, że programista może bardzo łatwo wykonywać międzyoperacyjność, ale wiąże się to z ceną wydajności. W przypadku przejścia należy wykonać następujące czynności:

  • Wykonywanie marshalingu danych
  • Naprawianie konwencji wywoływania
  • Ochrona rejestrów zapisanych przy wywoływaniu
  • Przełącz tryb wątku, aby GC nie blokowała niezarządzanych wątków
  • Wznoszenie ramki obsługi wyjątków dla wywołań do kodu zarządzanego
  • Przejmij kontrolę nad wątkiem (opcjonalnie)

Aby przyspieszyć czas przejścia, spróbuj użyć funkcji P/Invoke, gdy to możliwe. Obciążenie wynosi zaledwie 31 instrukcji oraz koszt marshallingu, jeśli wymagane jest marshalling danych, a w przeciwnym razie tylko 8. Międzyoperacyjna COM jest znacznie droższa, biorąc pod uwagę ponad 65 instrukcji.

Marshalling danych nie zawsze jest kosztowny. Typy pierwotne nie wymagają niemal żadnego marshallingu, a klasy z jawnym układem są również tanie. Rzeczywiste spowolnienie występuje podczas tłumaczenia danych, takiego jak konwersja tekstu z asCI na Unicode. Upewnij się, że dane, które są przekazywane przez zarządzaną granicę, są konwertowane tylko wtedy, gdy muszą być: może się okazać, że po prostu zgadzając się na określony typ danych lub format w programie, można wyciąć wiele obciążeń związanych z marshallingiem.

Następujące typy są nazywane blittable, co oznacza, że można je skopiować bezpośrednio przez zarządzaną/niezarządzaną granicę bez żadnego marshalla: sbyte, byte, short, ushort, int, uint, long, ulong, float i double. Można je przekazać bezpłatnie, a także valueTypes i tablice jednowymiarowe zawierające typy blittable. Szczegółowe informacje o marshallingu można dokładniej zbadać w bibliotece MSDN. Polecam uważnie czytać, jeśli spędzasz dużo czasu marshalling.

Projektowanie z wartością ValueTypes

Używaj prostych struktur, gdy możesz, a kiedy nie wykonujesz wielu bokserek i rozpatruj. Oto prosty przykład przedstawiający różnicę szybkości:

przy użyciu systemu;

przestrzeń nazw ConsoleApplication{

  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 50000000; i++)
      {foo test = new foo(3.14);}
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 50000000; i++)
      {bar test2 = new bar(3.14); }
      System.Console.WriteLine("All done");
    }
  }
}

Po uruchomieniu tego przykładu zobaczysz, że pętla struktury jest o rzędach wielkości szybciej. Należy jednak pamiętać o używaniu atrybutów ValueTypes podczas traktowania ich jak obiektów. To dodaje dodatkowe bokse i rozpieczętowywanie nad głową do programu, a w końcu może kosztować cię więcej niż gdyby utknął z obiektami! Aby zobaczyć to w akcji, zmodyfikuj powyższy kod, aby użyć tablicy foos i barów. Przekonasz się, że wydajność jest mniejsza lub mniejsza.

Kompromisów ValueTypes są znacznie mniej elastyczne niż obiekty i w końcu boli wydajność, jeśli jest używana niepoprawnie. Należy bardzo uważać na to, kiedy i jak ich używasz.

Spróbuj zmodyfikować powyższy przykład i przechowywać foos i paski wewnątrz tablic lub tabel skrótów. Zobaczysz zniknięcie przyrostu prędkości, tylko z jedną operacją boksu i rozpałkania.

Możesz śledzić, jak mocno jest pole i rozpakowanie, patrząc na alokacje i kolekcje GC. Można to zrobić przy użyciu wydajności zewnętrznej lub liczników wydajności w kodzie.

Zapoznaj się ze szczegółowym omówieniem wartości ValueTypes w temacie Zagadnienia dotyczące wydajności technologii Run-Time w .NET Framework.

Dodawanie grup za pomocą funkcji AddRange

Użyj funkcji AddRange , aby dodać całą kolekcję, zamiast dodawać każdy element w kolekcji iteracyjnie. Prawie wszystkie kontrolki i kolekcje okien mają metody Add i AddRange , a każda z nich jest zoptymalizowana do innego celu. Dodawanie jest przydatne podczas dodawania pojedynczego elementu, natomiast dodatek AddRange ma pewne dodatkowe obciążenie, ale wygrywa podczas dodawania wielu elementów. Oto kilka klas, które obsługują dodawanie i dodawanierange:

  • StringCollection, TraceCollection itp.
  • HttpWebRequest
  • Usercontrol
  • Columnheader

Przycinanie zestawu roboczego

Zminimalizuj liczbę zestawów, których używasz, aby zachować mały zestaw roboczy. Jeśli załadujesz cały zestaw tylko do użycia jednej metody, płacisz ogromny koszt za bardzo mało korzyści. Sprawdź, czy można zduplikować funkcjonalność tej metody przy użyciu kodu, który został już załadowany.

Śledzenie zestawu roboczego jest trudne i prawdopodobnie może być przedmiotem całego papieru. Poniżej przedstawiono kilka wskazówek, które pomogą Ci:

  • Użyj vadump.exe do śledzenia zestawu roboczego. Zostało to omówione w innym białym dokumencie obejmującym różne narzędzia dla środowiska zarządzanego.
  • Przyjrzyj się licznikom wydajności lub wydajności. Mogą one przekazać szczegółowe informacje o liczbie załadowanych klas lub liczbie metod, które otrzymują jiTed. Możesz uzyskać odczyty dotyczące ilości czasu spędzonego w module ładującym lub procentu czasu wykonywania spędzonego na stronicowanie.

Używanie pętli dla iteracji ciągów — wersja 1

W języku C# słowo kluczowe foreach umożliwia przejście między elementami na liście, ciągu itp. i wykonywać operacje na każdym elemencie. Jest to bardzo zaawansowane narzędzie, ponieważ działa jako moduł wyliczający ogólnego przeznaczenia w wielu typach. Kompromisem dla tej uogólnienia jest szybkość, a jeśli polegasz mocno na iteracji ciągów, należy zamiast tego użyć pętli For . Ponieważ ciągi są prostymi tablicami znaków, można je przechodzić przy użyciu znacznie mniejszego obciążenia niż inne struktury. Funkcja JIT jest wystarczająco inteligentna (w wielu przypadkach), aby zoptymalizować sprawdzanie granic i inne rzeczy wewnątrz pętli For , ale nie można tego robić na spacerach foreach . Wynikiem końcowym jest to, że w wersji 1 pętla For dla ciągów jest do pięciu razy szybsza niż w przypadku użycia foreach. Zmieni się to w przyszłych wersjach, ale w przypadku wersji 1 jest to określony sposób zwiększenia wydajności.

Oto prosta metoda testowa, która pokazuje różnicę szybkości. Spróbuj go uruchomić, a następnie usuń pętlę For i usuń komentarz instrukcji foreach . Na mojej maszynie pętla For trwała około sekundy, z około 3 sekundy instrukcji foreach .

public static void Main(string[] args) {
  string s = "monkeys!";
  int dummy = 0;

  System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
  for(int i = 0; i < 1000000; i++)
    sb.Append(s);
  s = sb.ToString();
  //foreach (char c in s) dummy++;
  for (int i = 0; i < 1000000; i++)
    dummy++;
  return;   
  }
}

KompromisyForeach są znacznie bardziej czytelne, a w przyszłości stanie się tak szybko, jak pętla For dla specjalnych przypadków, takich jak ciągi. Chyba że manipulowanie ciągami jest prawdziwym hogem wydajności dla Ciebie, nieco niechlujny kod może nie być wart.

Używanie narzędzia StringBuilder do manipulowania ciągami złożonymi

Po zmodyfikowaniu ciągu czas wykonywania utworzy nowy ciąg i zwróci go, pozostawiając oryginalny element do usunięcia pamięci. W większości przypadków jest to szybki i prosty sposób, aby to zrobić, ale gdy ciąg jest modyfikowany wielokrotnie, zaczyna być obciążeniem dla wydajności: wszystkie te alokacje w końcu się kosztują. Oto prosty przykład programu dołączanego do ciągu 50 000 razy, po którym następuje jeden, który używa obiektu StringBuilder do modyfikowania ciągu. Kod StringBuilder jest znacznie szybszy i jeśli je uruchomisz, staje się natychmiast oczywiste.

namespace ConsoleApplication1.Feedback{
  using System;
  
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      String str = test.text;
      for(int i=0;i<50000;i++){
        str = str + "blue_toothbrush";
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}
namespace ConsoleApplication1.Feedback{
  using System;
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      System.Text.StringBuilder SB = 
        new System.Text.StringBuilder(test.text);
      for(int i=0;i<50000;i++){
        SB.Append("blue_toothbrush");
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}

Spróbuj spojrzeć na Narzędzie Perfmon, aby zobaczyć, ile czasu jest zapisywane bez przydzielania tysięcy ciągów. Przyjrzyj się licznikowi "% czasu w GC" na liście pamięci środowiska CLR platformy .NET. Można również śledzić liczbę zapisanych alokacji, a także statystyki kolekcji.

Kompromisy= Istnieje pewne obciążenie związane z tworzeniem obiektu StringBuilder , zarówno w czasie, jak i w pamięci. Na maszynie z szybką pamięcią funkcja StringBuilder staje się warta, jeśli wykonujesz około pięciu operacji. Jako reguła kciuka, powiedziałbym, że 10 lub więcej operacji ciągów jest uzasadnieniem obciążenia na każdej maszynie, nawet wolniej.

Prekompiluj aplikacje Windows Forms

Metody są JITed, gdy są używane po raz pierwszy, co oznacza, że płacisz większą karę za uruchomienie, jeśli aplikacja wykonuje wiele metod wywołujących podczas uruchamiania. Windows Forms używać wielu bibliotek udostępnionych w systemie operacyjnym, a obciążenie podczas uruchamiania ich może być znacznie wyższe niż inne rodzaje aplikacji. Chociaż nie zawsze tak jest, wstępne komkompilowanie Windows Forms aplikacji zwykle powoduje wygraną wydajności. W innych scenariuszach zwykle najlepiej jest pozwolić, aby JIT zająć się nim, ale jeśli jesteś Windows Forms deweloperem, możesz spojrzeć.

Firma Microsoft pozwala wstępnie skompilować aplikację przez wywołanie metody ngen.exe. Możesz uruchomić ngen.exe w czasie instalacji lub przed dystrybucją aplikacji. Zdecydowanie najlepiej jest uruchamiać ngen.exe w czasie instalacji, ponieważ można upewnić się, że aplikacja jest zoptymalizowana pod kątem maszyny, na której jest zainstalowana. Jeśli uruchomisz ngen.exe przed wysłaniem programu, ograniczysz optymalizacje do tych, które są dostępne na maszynie . Aby zapoznać się z tym, ile wstępnie skompilować, mogę uruchomić nieformalny test na mojej maszynie. Poniżej przedstawiono czasy zimnego uruchamiania programu ShowFormComplex, aplikacji winforms z około setką kontrolek.

Stan kodu Godzina
Struktura JITed

ShowFormComplex JITed

3,4 s
Struktura wstępnie skompilowana, ShowFormComplex JITed 2,5 s
Struktura wstępnie skompilowana, Prekompilowana struktura ShowFormComplex 2.1sec

Każdy test został wykonany po ponownym uruchomieniu. Jak widać, Windows Forms aplikacje korzystają z wielu metod z góry, dzięki czemu jest to znacząca wygrana wydajności, aby wstępnie skompilować.

Korzystanie z tablic pognieżdżonych — wersja 1

Interfejs JIT w wersji 1 optymalizuje poszarpane tablice (po prostu "tablice tablice) wydajniej niż prostokątne tablice, a różnica jest dość zauważalna. Oto tabela przedstawiająca wzrost wydajności wynikający z używania poszarpanych tablic zamiast prostokątnych w języku C# i Visual Basic (większa liczba jest lepsza):

  C# Visual Basic 7
Przypisanie (poszarpane)

Przypisanie (prostokątne)

14.16

8.37

12.24

8.62

Sieć neuronowa (poszarpana)

Sieć neuronowa (prostokątna)

4.48

3,00

4.58

3.13

Sortowanie liczbowe (poszarpane)

Sortowanie liczbowe (prostokątne)

4.88

2.05

5.07

2.06

Test porównawczy przypisania jest prostym algorytmem przypisania dostosowanym z przewodnika krok po kroku znalezionego w temacie Ilościowe podejmowanie decyzji dla firm (Gordon, Pressman i Cohn; Prentice-Hall; poza drukowaniem). Test sieci neuronowej uruchamia serię wzorców w małej sieci neuronowej, a sortowanie liczbowe jest objaśniające. Połączone te testy porównawcze stanowią dobre wskazanie rzeczywistej wydajności.

Jak widać, użycie poszarpanych tablic może spowodować dość dramatyczny wzrost wydajności. Optymalizacje wykonane w przypadku poszarpanych tablic zostaną dodane do przyszłych wersji JIT, ale dla wersji 1 można zaoszczędzić dużo czasu przy użyciu poszarpanych tablic.

Zachowaj rozmiar buforu we/wy z przedziału od 4 KB do 8 KB

W przypadku prawie każdej aplikacji bufor o rozmiarze od 4 KB do 8 KB zapewni maksymalną wydajność. W przypadku bardzo konkretnych wystąpień możesz uzyskać poprawę z większego buforu (ładowanie dużych obrazów o przewidywalnym rozmiarze, na przykład), ale w 99,99% przypadków będzie marnować tylko pamięć. Wszystkie bufory pochodzące z bufferedStream umożliwiają ustawienie rozmiaru na dowolny rozmiar, ale w większości przypadków 4 i 8 zapewni najlepszą wydajność.

Bądź na platformie Lookout for Asynchronous IO Opportunities

W rzadkich przypadkach możesz skorzystać z asynchronicznego we/wy. Jednym z przykładów może być pobieranie i dekompresowanie serii plików: można odczytywać bity z jednego strumienia, dekodować je na procesorze CPU i zapisywać je na innym. Użycie asynchronicznego we/wy wymaga dużo wysiłku i może spowodować utratę wydajności, jeśli nie zostanie wykonana prawidłowo. Zaletą jest to, że w przypadku poprawnego zastosowania asynchroniczne operacje we/wy mogą dać ci nawet dziesięć razy większą wydajność.

Doskonały przykład programu korzystającego z asynchronicznego we /wy jest dostępny w bibliotece MSDN.

  • Należy zauważyć, że istnieje niewielkie obciążenie zabezpieczeń dla wywołań asynchronicznych: podczas wywoływania wywołania asynchronicznego stan zabezpieczeń stosu obiektu wywołującego jest przechwytywany i przesyłany do wątku, który faktycznie wykona żądanie. Może to nie być problemem, jeśli wywołanie zwrotne wykonuje dużo kodu lub jeśli wywołania asynchroniczne nie są nadmiernie używane

Porady dotyczące dostępu do bazy danych

Filozofią dostrajania dostępu do bazy danych jest użycie tylko potrzebnych funkcji i zaprojektowanie wokół podejścia "rozłączone": wykonaj kilka połączeń w sekwencji, zamiast utrzymywać pojedyncze połączenie otwarte przez długi czas. Należy wziąć to pod uwagę i zaprojektować wokół niej.

Firma Microsoft zaleca strategię N-warstwową w celu uzyskania maksymalnej wydajności, w przeciwieństwie do bezpośredniego połączenia klient-baza danych. Weź to pod uwagę w ramach filozofii projektowania, ponieważ wiele twoich technologii jest zoptymalizowanych pod kątem korzystania ze scenariusza wielomęcznego.

Korzystanie z optymalnego dostawcy zarządzanego

Wybierz odpowiedniego dostawcę zarządzanego, zamiast polegać na ogólnym metodzie dostępu. Istnieją dostawcy zarządzani napisani specjalnie dla wielu różnych baz danych, takich jak SQL (System.Data.SqlClient). Jeśli używasz bardziej ogólnego interfejsu, takiego jak System.Data.Odbc, gdy można użyć wyspecjalizowanego składnika, utracisz wydajność pracy z dodanym poziomem pośrednim. Użycie optymalnego dostawcy może również mieć inny język: zarządzany klient SQL mówi TDS do bazy danych SQL, zapewniając dramatyczną poprawę ogólnej oledbprotocol.

Wybierz czytnik danych w zestawie danych, gdy możesz

Używaj czytnika danych za każdym razem, gdy nie musisz przechowywać danych w pobliżu. Dzięki temu można szybko odczytywać dane, które mogą być buforowane, jeśli użytkownik chce. Czytnik jest po prostu strumieniem bezstanowym, który umożliwia odczytywanie danych w miarę ich nadejścia, a następnie upuszczanie go bez przechowywania ich w zestawie danych w celu uzyskania większej ilości nawigacji. Podejście do strumienia jest szybsze i ma mniejsze obciążenie, ponieważ można natychmiast rozpocząć korzystanie z danych. Należy ocenić, jak często potrzebujesz tych samych danych, aby zdecydować, czy buforowanie na potrzeby nawigacji ma sens. Oto mała tabela przedstawiająca różnicę między elementami DataReader i DataSet dla dostawców ODBC i SQL podczas ściągania danych z serwera (większa liczba jest lepsza):

  ADO SQL
DataSet 801 2507
Datareader 1083 4585

Jak widać, najwyższa wydajność jest osiągana w przypadku korzystania z optymalnego zarządzanego dostawcy wraz z czytnikiem danych. Jeśli nie musisz buforować danych, użycie czytnika danych może zapewnić ogromny wzrost wydajności.

Używanie Mscorsvr.dll dla maszyn MP Machines

W przypadku autonomicznych aplikacji warstwy środkowej i serwerów upewnij się, że mscorsvr jest używana w przypadku maszyn wieloprocesorowych. Rozwiązanie Mscorwks nie jest zoptymalizowane pod kątem skalowania ani przepływności, podczas gdy wersja serwera ma kilka optymalizacji, które umożliwiają jej skalowanie w przypadku dostępności więcej niż jednego procesora.

Zawsze, gdy jest to możliwe, używaj procedur składowanych

Procedury składowane to wysoce zoptymalizowane narzędzia, które zapewniają doskonałą wydajność podczas efektywnego używania. Skonfiguruj procedury składowane do obsługi wstawiania, aktualizacji i usuwania za pomocą karty danych. Procedury składowane nie muszą być interpretowane, kompilowane, a nawet przesyłane z klienta i ograniczać obciążenie zarówno ruchu sieciowego, jak i serwera. Pamiętaj, aby użyć właściwości CommandType.StoredProcedure zamiast CommandType.Text

Zachowaj ostrożność przy dynamicznych parametrach połączenia

Buforowanie połączeń to przydatny sposób ponownego użycia połączeń dla wielu żądań, zamiast płacić narzut związany z otwieraniem i zamykaniem połączenia dla każdego żądania. Odbywa się to niejawnie, ale otrzymujesz jedną pulę na unikatowe parametry połączenia. Jeśli generujesz dynamicznie parametry połączenia, upewnij się, że parametry są identyczne za każdym razem, aby odbywało się buforowanie. Należy również pamiętać, że jeśli delegowanie występuje, otrzymasz jedną pulę na użytkownika. Istnieje wiele opcji, które można ustawić dla puli połączeń i można śledzić wydajność puli przy użyciu narzędzia Perfmon, aby śledzić elementy, takie jak czas odpowiedzi, transakcje/s itp.

Wyłączanie funkcji, których nie używasz

Wyłącz automatyczne rejestrowanie transakcji, jeśli nie jest to konieczne. W przypadku dostawcy zarządzanego SQL odbywa się to za pośrednictwem parametrów połączenia:

SqlConnection conn = new SqlConnection(
"Server=mysrv01;
Integrated Security=true;
Enlist=false");

Podczas wypełniania zestawu danych za pomocą karty danych nie pobieraj informacji o kluczu podstawowym, jeśli nie musisz (np. nie ustawiaj opcji MissingSchemaAction.Add z kluczem):

public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
    SqlConnection conn = new SqlConnection(connection);
    SqlDataAdapter adapter = new SqlDataAdapter();
    adapter.SelectCommand = new SqlCommand(query, conn);
    adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
    adapter.Fill(dataset);
    return dataset;
}

Unikanie poleceń generowanych automatycznie

W przypadku korzystania z adaptera danych unikaj poleceń generowanych automatycznie. Wymagają one dodatkowych podróży do serwera w celu pobrania metadanych i zapewniania niższego poziomu kontroli interakcji. Podczas korzystania z automatycznie generowanych poleceń warto wykonać to samodzielnie w aplikacjach o krytycznym znaczeniu dla wydajności.

Uważaj, ADO Legacy Design

Należy pamiętać, że po wykonaniu polecenia lub wywołaniu wypełnienia na karcie jest zwracany każdy rekord określony przez zapytanie.

Jeśli kursory serwera są absolutnie wymagane, można je zaimplementować za pomocą procedury składowanej w języku t-sql. Unikaj, jeśli jest to możliwe, ponieważ implementacje oparte na kursorach serwera nie są bardzo dobrze skalowane.

W razie potrzeby zaimplementuj stronicowanie w sposób bezstanowy i bez połączenia. Do zestawu danych można dodać dodatkowe rekordy, wykonując następujące czynności:

  • Upewniając się, że informacje PK są obecne
  • Zmiana polecenia select karty danych zgodnie z potrzebami i
  • Wywoływanie wypełnienia

Zachowaj swoje zestawy danych lean

Umieść tylko rekordy potrzebne w zestawie danych. Należy pamiętać, że zestaw danych przechowuje wszystkie jego dane w pamięci i że tym więcej żądanych danych będzie trwać dłużej.

Używaj dostępu sekwencyjnego tak często, jak to możliwe

W przypadku czytnika danych użyj polecenia CommandBehavior.SequentialAccess. Jest to niezbędne do obsługi typów danych obiektów blob, ponieważ umożliwia odczytywanie danych z przewodu w małych fragmentach. Chociaż w danym momencie można pracować tylko z jednym elementem danych, opóźnienie ładowania dużego typu danych znika. Jeśli nie musisz jednocześnie pracować z całym obiektem, użycie dostępu sekwencyjnego zapewni znacznie lepszą wydajność.

Porady dotyczące wydajności aplikacji ASP.NET

Pamięć podręczna agresywnie

Podczas projektowania aplikacji przy użyciu ASP.NET upewnij się, że projektujesz z myślą o buforowaniu. W wersjach serwera systemu operacyjnego istnieje wiele opcji dostosowywania użycia pamięci podręcznych po stronie serwera i klienta. Istnieje kilka funkcji i narzędzi na platformie ASP, których można użyć do uzyskania wydajności.

Buforowanie danych wyjściowych — przechowuje statyczny wynik żądania ASP. Określona <@% OutputCache %> przy użyciu dyrektywy :

  • Czas trwania — element czasu istnieje w pamięci podręcznej
  • VaryByParam — zmienia wpisy pamięci podręcznej według parametrów Get/Post
  • VaryByHeader — zmienia wpisy pamięci podręcznej według nagłówka Http
  • VaryByCustom — zmienia wpisy pamięci podręcznej według przeglądarki
  • Przesłoń, aby różnić się w zależności od potrzeb:
    • Buforowanie fragmentów — gdy nie jest możliwe przechowywanie całej strony (prywatność, personalizacja, zawartość dynamiczna), można użyć buforowania fragmentów do przechowywania części w celu późniejszego szybszego pobierania.

      a) VaryByControl — zmienia buforowane elementy według wartości kontrolki

    • Interfejs API pamięci podręcznej — zapewnia bardzo szczegółowy stopień szczegółowości buforowania przez przechowywanie tabeli skrótów buforowanych obiektów w pamięci (System.web.UI.caching). Ponadto:

      a) Obejmuje zależności (klucz, plik, czas)

      b) Automatycznie wygasa nieużywane elementy

      c) Obsługuje wywołania zwrotne

Buforowanie inteligentnie może zapewnić doskonałą wydajność i ważne jest, aby zastanowić się, jakiego rodzaju buforowanie potrzebujesz. Wyobraź sobie złożoną witrynę handlu elektronicznego z kilkoma statycznymi stronami logowania, a następnie wysadziła dynamicznie generowane strony zawierające obrazy i tekst. Możesz chcieć użyć buforowania danych wyjściowych dla tych stron logowania, a następnie buforowania fragmentów dla stron dynamicznych. Na przykład pasek narzędzi może być buforowany jako fragment. Aby zapewnić jeszcze lepszą wydajność, można buforować często używane obrazy i standardowy tekst, który jest często wyświetlany w witrynie przy użyciu interfejsu API pamięci podręcznej. Aby uzyskać szczegółowe informacje na temat buforowania (z przykładowym kodem), zapoznaj się z witryną internetową platformy ASP. NET.

Używaj stanu sesji tylko wtedy, gdy musisz

Jedną z niezwykle zaawansowanych funkcji ASP.NET jest możliwość przechowywania stanu sesji dla użytkowników, takich jak koszyk na stronie handlu elektronicznego lub historia przeglądarki. Ponieważ ta opcja jest domyślnie włączona, płacisz koszt w pamięci, nawet jeśli go nie używasz. Jeśli nie używasz stanu sesji, wyłącz go i zaoszczędź sobie obciążenie, dodając <@% EnabledSessionState = false %> do asp. Jest to kilka innych opcji, które zostały wyjaśnione w witrynie sieci Web platformy ASP. NET.

W przypadku stron, które tylko odczytują stan sesji, możesz wybrać pozycję EnabledSessionState=readonly. Zapewnia to mniejsze obciążenie niż pełny stan sesji odczytu/zapisu i jest przydatne, gdy potrzebujesz tylko części funkcji i nie chcesz płacić za możliwości zapisu.

Użyj stanu widoku tylko wtedy, gdy musisz

Przykładem stanu widoku może być długi formularz, który użytkownicy muszą wypełnić: jeśli kliknie przycisk Wstecz w przeglądarce, a następnie zwróci formularz, formularz pozostanie wypełniony. Jeśli ta funkcja nie jest używana, ten stan powoduje usunięcie pamięci i wydajności. Być może największy drenaż wydajności polega na tym, że sygnał rundy musi być wysyłany przez sieć za każdym razem, gdy strona jest ładowana do aktualizacji i weryfikowania pamięci podręcznej. Ponieważ jest on domyślnie włączony, należy określić, że nie chcesz używać stanu widoku z <@% EnabledViewState = false %>. Aby dowiedzieć się więcej o innych opcjach i ustawieniach, do których masz dostęp, przeczytaj więcej o stanie wyświetlania w witrynie internetowej ASP. NET.

Unikaj modelu STA COM

Apartment COM jest przeznaczony do obsługi wątków w środowiskach niezarządzanych. Istnieją dwa rodzaje modelu Apartment COM: jednowątkowy i wielowątkowy. MtA COM jest przeznaczony do obsługi wielowątków, podczas gdy STA COM opiera się na systemie obsługi komunikatów w celu serializacji żądań wątków. Zarządzany świat jest bezwątkowy, a korzystanie z modelu Single Threaded Apartment COM wymaga, aby wszystkie niezarządzane wątki zasadniczo współdzieliły jeden wątek dla międzyoperacyjności. Skutkuje to ogromnym trafieniem wydajności i należy unikać go zawsze, gdy jest to możliwe. Jeśli nie możesz przekazać obiektu Apartment COM do zarządzanego świata, użyj <elementu @%AspCompat = "true" %> dla stron, które ich używają. Aby uzyskać bardziej szczegółowe wyjaśnienie modelu STA COM, zobacz bibliotekę MSDN.

Kompilowanie wsadowe

Zawsze kompiluj wsadowe przed wdrożeniem dużej strony w sieci Web. Można to zainicjować, wykonując jedno żądanie do strony dla każdego katalogu i czekając na ponowne przełączenie procesora CPU w stan bezczynności. Dzięki temu serwer sieci Web nie może być sfałszowany kompilacjami, a jednocześnie próbuje obsłużyć strony.

Usuwanie niepotrzebnych modułów HTTP

W zależności od używanych funkcji usuń nieużywane lub niepotrzebne moduły http z potoku. Odzyskanie dodanej pamięci i zmarnowanych cykli może zapewnić niewielki wzrost prędkości.

Unikaj funkcji Autoeventwireup

Zamiast polegać na autoeventwireup, przesłoń zdarzenia ze strony. Na przykład zamiast pisać metodę Page_Load(), spróbuj przeciążyć metodę public void OnLoad(). Dzięki temu czas wykonywania może mieć możliwość wykonania polecenia CreateDelegate() dla każdej strony.

Kodowanie przy użyciu ASCII, gdy nie potrzebujesz utF

Domyślnie usługa ASP.NET jest skonfigurowana do kodowania żądań i odpowiedzi jako UTF-8. Jeśli ASCII jest wszystkie potrzeby twojej aplikacji, wyeliminowane obciążenie UTF może dać z powrotem kilka cykli. Należy pamiętać, że można to zrobić tylko dla poszczególnych aplikacji.

Korzystanie z procedury optymalnego uwierzytelniania

Istnieje kilka różnych sposobów uwierzytelniania użytkownika i niektórych droższych niż inne (w celu zwiększenia kosztów: None, Windows, Forms, Passport). Upewnij się, że używasz najtańszej, która najlepiej odpowiada Twoim potrzebom.

Porady dotyczące przenoszenia i opracowywania w Visual Basic

Wiele zmieniło się pod maską z microsoft® Visual Basic® 6 do Microsoft® Visual Basic® 7, a mapa wydajności zmieniła się wraz z nim. Ze względu na dodatkowe funkcje i ograniczenia zabezpieczeń środowiska CLR niektóre funkcje po prostu nie mogą działać tak szybko, jak w visual Basic 6. W rzeczywistości istnieje kilka obszarów, w których program Visual Basic 7 zostaje odbity przez jego poprzednika. Na szczęście są dwie dobre wiadomości:

  • Większość najgorszych spowolnień występuje podczas jednorazowych funkcji, takich jak ładowanie kontrolki po raz pierwszy. Koszt jest tam, ale płacisz go tylko raz.
  • Istnieje wiele obszarów, w których program Visual Basic 7 jest szybszy, a te obszary zwykle leżą w funkcjach powtarzanych w czasie wykonywania. Oznacza to, że korzyść rośnie wraz z upływem czasu, a w kilku przypadkach będzie przewyższać jednorazowe koszty.

Większość problemów z wydajnością pochodzi z obszarów, w których czas wykonywania nie obsługuje funkcji języka Visual Basic 6 i należy ją dodać, aby zachować funkcję w visual basic 7. Praca poza czasem wykonywania jest wolniejsza, dzięki czemu niektóre funkcje są znacznie droższe. Jasna strona polega na tym, że można uniknąć tych problemów z odrobiną wysiłku. Istnieją dwa główne obszary, które wymagają pracy w celu zoptymalizowania pod kątem wydajności, i kilka prostych poprawek, które można wykonać tutaj i tam. W połączeniu te elementy mogą pomóc w obejście opróżnień wydajności i skorzystać z funkcji, które są znacznie szybsze w języku Visual Basic 7.

Obsługa błędów

Pierwszym problemem jest obsługa błędów. Wiele się zmieniło w programie Visual Basic 7 i występują problemy z wydajnością związane ze zmianą. Zasadniczo logika wymagana do zaimplementowania polecenia OnErrorGoto i Resume jest niezwykle kosztowna. Zalecam szybkie przyjrzenie się kodowi i wyróżnienie wszystkich obszarów, w których używasz obiektu Err lub dowolnego mechanizmu obsługi błędów. Teraz przyjrzyj się każdemu z tych wystąpień i sprawdź, czy możesz je ponownie napisać, aby użyć funkcji try/catch. Wielu deweloperów okaże się, że mogą łatwo konwertować na try/catch w większości przypadków i powinni zobaczyć dobrą poprawę wydajności w swoim programie. Reguła kciuka to "jeśli można łatwo zobaczyć tłumaczenie, zrób to".

Oto przykład prostego programu Visual Basic, który używa polecenia On Error Goto w porównaniu z wersją try/catch .

Sub SubWithError()
On Error Goto SWETrap
  Dim x As Integer
  Dim y As Integer
  x = x / y
SWETrap:  Exit Sub
  End Sub
 
Sub SubWithErrorResumeLabel()
  On Error Goto SWERLTrap
  Dim x As Integer
  Dim y As Integer
  x = x / y 
SWERLTrap:
  Resume SWERLExit
  End Sub
SWERLExit:
  Exit Sub
Sub SubWithError()
  Dim x As Integer
  Dim y As Integer
  Try    x = x / y  Catch    Return  End Try
  End Sub
 
Sub SubWithErrorResumeLabel()
  Dim x As Integer
  Dim y As Integer
  Try
    x = x / y
  Catch
  Goto SWERLExit
  End Try
 
SWERLExit:
  Return
  End Sub

Wzrost prędkości jest zauważalny. SubWithError() przyjmuje 244 milisekundy przy użyciu polecenia OnErrorGoto i tylko 169 milisekund przy użyciu try/catch. Druga funkcja przyjmuje 179 milisekund w porównaniu do 164 milisekund dla zoptymalizowanej wersji.

Korzystanie z wczesnego powiązania

Druga kwestia dotyczy obiektów i emisji typów. Visual Basic 6 wykonuje wiele pracy pod maską, aby obsługiwać rzutowanie obiektów, a wielu programistów nawet nie zdaje sobie z tego sprawy. W visual Basic 7 jest to obszar, z którego można wycisnąć dużo wydajności. Podczas kompilowania użyj wczesnego powiązania. Nakazuje to kompilatorowi wstawienie przymusu typu tylko wtedy, gdy zostanie jawnie wymienione. Ma to dwa główne skutki:

  • Dziwne błędy stają się łatwiejsze do śledzenia.
  • Niepotrzebne przymusy są wyeliminowane, co prowadzi do znacznych ulepszeń wydajności.

Jeśli używasz obiektu tak, jakby był to inny typ, język Visual Basic będzie coerce obiekt dla Ciebie, jeśli nie zostanie określony. Jest to przydatne, ponieważ programista musi martwić się o mniej kodu. Wadą jest to, że te przymusy mogą robić nieoczekiwane rzeczy, a programista nie ma nad nimi kontroli.

Istnieją wystąpienia, gdy trzeba używać późnego powiązania, ale w większości przypadków, jeśli nie masz pewności, możesz uciec z wczesnym powiązaniem. W przypadku programistów języka Visual Basic 6 może to być na początku nieco niezręczne, ponieważ musisz martwić się o typy więcej niż w przeszłości. Powinno to być łatwe dla nowych programistów, a osoby zaznajomione z Visual Basic 6 będą pobierać go bez czasu.

Włącz opcję ścisłą i jawną

W przypadku opcji ściśle włączonej chronisz się przed nieumyślnym późnym powiązaniem i wymuszasz wyższy poziom dyscypliny kodowania. Aby zapoznać się z listą ograniczeń obecnych za pomocą opcji Strict, zobacz bibliotekę MSDN. Zastrzeżeniem jest to, że wszystkie zawężanie typów muszą być jawnie określone. Jednak samo w sobie może odkryć inne sekcje kodu, które wykonują więcej pracy niż wcześniej myślisz, i może to pomóc w stłumieniu niektórych usterek w procesie.

Opcja Jawna jest mniej restrykcyjna niż Opcja Strict, ale nadal wymusza programistom podanie większej ilości informacji w kodzie. W szczególności należy zadeklarować zmienną przed jej użyciem. Spowoduje to przeniesienie wnioskowania typu z czasu wykonywania do czasu kompilacji. Ta wyeliminowana kontrola przekłada się na dodatkową wydajność.

Zalecamy rozpoczęcie od opcji Jawne, a następnie włączenie opcji Ścisłe. Zapewni to ochronę przed potopem błędów kompilatora i umożliwi stopniowe rozpoczęcie pracy w bardziej rygorystycznym środowisku. W przypadku użycia obu tych opcji zapewniasz maksymalną wydajność aplikacji.

Używanie porównania binarnego dla tekstu

Podczas porównywania tekstu użyj porównania binarnego zamiast porównywania tekstu. W czasie wykonywania obciążenie jest znacznie lżejsze dla danych binarnych.

Minimalizuj użycie formatu()

Gdy to możliwe, użyj polecenia toString() zamiast formatu(). W większości przypadków zapewnia ona potrzebne funkcje z znacznie mniejszym obciążeniem.

Korzystanie z znaku Charw

Użyj znaku charw zamiast znaku. ClR używa formatu Unicode wewnętrznie, a znak musi zostać przetłumaczony w czasie wykonywania, jeśli jest używany. Może to spowodować znaczną utratę wydajności i określenie, że znaki są pełnym słowem (przy użyciu znaku charw) eliminuje tę konwersję.

Optymalizowanie przypisań

Użyj exp += val zamiast exp = exp + val. Ponieważ exp może być dowolnie złożone, może to spowodować wiele niepotrzebnych prac. Wymusza to, aby funkcja JIT oceniała obie kopie eksploatywne i wiele razy nie jest to konieczne. Pierwsza instrukcja może być zoptymalizowana znacznie lepiej niż druga, ponieważ JIT może uniknąć dwukrotnego oszacowania exp .

Unikaj niepotrzebnego pośredniego

W przypadku używania elementu byRef przekazuje się wskaźniki zamiast rzeczywistego obiektu. Wiele razy ma to sens (na przykład funkcje efekt uboczny), ale nie zawsze go potrzebujesz. Wskaźniki przekazujące skutkują bardziej pośrednim, co jest wolniejsze niż uzyskiwanie dostępu do wartości, która znajduje się na stosie. Jeśli nie musisz przechodzić przez stertę, najlepiej jest go uniknąć.

Umieszczanie łączenia w jednym wyrażeniu

Jeśli masz wiele łączy w wielu wierszach, spróbuj trzymać je wszystkie na jednym wyrażeniu. Kompilator może zoptymalizować, modyfikując ciąg w miejscu, zapewniając szybkość i zwiększenie pamięci. Jeśli instrukcje są podzielone na wiele wierszy, kompilator języka Visual Basic nie wygeneruje języka Microsoft Intermediate Language (MSIL), aby umożliwić łączenie w miejscu. Zobacz przykład StringBuilder omówiony wcześniej.

Dołączanie instrukcji zwracanych

Visual Basic umożliwia funkcji zwracanie wartości bez użycia instrukcji return . Chociaż program Visual Basic 7 obsługuje tę funkcję, jawnie użycie funkcji return umożliwia JIT wykonywanie nieco większej optymalizacji. Bez instrukcji return każda funkcja otrzymuje kilka zmiennych lokalnych na stosie, aby przezroczystie obsługiwać zwracanie wartości bez słowa kluczowego. Utrzymywanie tych informacji utrudnia optymalizację trybu JIT i może mieć wpływ na wydajność kodu. Przejrzyj funkcje i wstaw powrót zgodnie z potrzebami. W ogóle nie zmienia semantyki kodu i może pomóc w szybciej z aplikacji.

Porady dotyczące przenoszenia i opracowywania w zarządzanym języku C++

Firma Microsoft jest przeznaczona dla zarządzanego języka C++ (MC++) w określonym zestawie deweloperów. MC++ nie jest najlepszym narzędziem dla każdego zadania. Po przeczytaniu tego dokumentu możesz zdecydować, że język C++ nie jest najlepszym narzędziem i że koszty kompromisu nie są warte korzyści. Jeśli nie masz pewności co do mc++, istnieje wiele dobrych zasobów , które pomogą Ci podjąć decyzję Ta sekcja jest przeznaczona dla deweloperów, którzy już zdecydowali, że chcą korzystać z mc++ w jakiś sposób i chcą wiedzieć o aspektach wydajności.

W przypadku deweloperów języka C++ praca w zarządzanym języku C++ wymaga podjęcia kilku decyzji. Czy przenosisz stary kod? Jeśli tak, czy chcesz przenieść całą przestrzeń zarządzaną, czy zamiast tego planujesz zaimplementować otokę? Skupię się na opcji "port-wszystko" lub zajmiemy się pisaniem mc++ od podstaw na potrzeby tej dyskusji, ponieważ są to scenariusze, w których programista zauważy różnicę wydajności.

Korzyści z zarządzanego świata

Najbardziej zaawansowaną funkcją zarządzanego języka C++ jest możliwość łączenia i dopasowywania zarządzanego i niezarządzanego kodu na poziomie wyrażenia. Żaden inny język nie pozwala na to, a istnieją pewne zaawansowane korzyści, które można uzyskać z niego, jeśli są używane prawidłowo. Omówię kilka przykładów tego później.

Zarządzany świat daje również ogromne zwycięstwa projektowe, w tym wiele typowych problemów są dbane o Ciebie. Zarządzanie pamięcią, planowanie wątków i wymuszanie typów mogą być pozostawione do czasu wykonywania, jeśli chcesz, co pozwala skoncentrować energię na częściach programu, które go potrzebują. Dzięki programowi MC++możesz wybrać dokładnie, ile kontroli chcesz zachować.

Programiści MC++ mają luksus korzystania z zaplecza microsoft Visual C® 7 (VC7) podczas kompilowania do IL, a następnie korzystania z JIT na tym. Programiści, którzy są przyzwyczajeni do pracy z kompilatorem Microsoft C++, są przyzwyczajeni do błyskawicznego działania. JIT został zaprojektowany z różnymi celami i ma inny zestaw mocnych i słabych stron. Kompilator VC7, niezwiązany przez ograniczenia czasowe JIT, może wykonać pewne optymalizacje, których nie można wykonać, na przykład analizy całego programu, bardziej agresywnego tworzenia i wyrejestrowania. Istnieją również pewne optymalizacje, które można wykonać tylko w środowiskach typusafe, pozostawiając więcej miejsca na szybkość niż pozwala C++.

Ze względu na różne priorytety w JIT niektóre operacje są szybsze niż wcześniej, podczas gdy inne są wolniejsze. Istnieją kompromisy, które sprawiają, że dla bezpieczeństwa i elastyczności języka, a niektóre z nich nie są tanie. Na szczęście istnieją rzeczy, które programista może zrobić, aby zminimalizować koszty.

Przenoszenie: cały kod C++ może kompilować się do MSIL

Zanim przejdziemy dalej, należy pamiętać, że można skompilować dowolny kod C++ w MSIL. Wszystko będzie działać, ale nie ma gwarancji bezpieczeństwa typu i płacisz karę marshalling, jeśli robisz wiele międzyoperacja. Dlaczego warto skompilować bibliotekę MSIL, jeśli nie uzyskasz żadnych korzyści? W sytuacjach, w których portujesz dużą bazę kodu, umożliwia to stopniowe przenoszenie kodu w fragmentach. Możesz poświęcić czas na przenoszenie większej ilości kodu, zamiast pisać specjalne otoki, aby skleić portowany i nie portowany kod razem, jeśli używasz MC++, a to może spowodować duże zwycięstwo. Sprawia to, że przenoszenie aplikacji jest bardzo czystym procesem. Aby dowiedzieć się więcej na temat kompilowania języka C++ do MSIL, zapoznaj się z opcją kompilatora /clr.

Jednak po prostu kompilowanie kodu C++ do MSIL nie zapewnia bezpieczeństwa ani elastyczności zarządzanego świata. Musisz napisać w programie MC++, a w wersji 1 oznacza to rezygnację z kilku funkcji. Poniższa lista nie jest obsługiwana w bieżącej wersji środowiska CLR, ale może być w przyszłości. Firma Microsoft zdecydowała się najpierw obsługiwać najbardziej typowe funkcje i musiała wyciąć kilka innych, aby wysłać. Nie ma nic, co uniemożliwia ich dodawanie później, ale w międzyczasie trzeba będzie to zrobić bez nich:

  • Dziedziczenie wielokrotne
  • Szablony
  • Deterministyczna finalizacja

Zawsze możesz współpracować z niebezpiecznym kodem, jeśli potrzebujesz tych funkcji, ale zapłacisz karę za wydajność danych marshallingu tam i z powrotem. Należy pamiętać, że te funkcje mogą być używane tylko wewnątrz niezarządzanego kodu. Zarządzana przestrzeń nie ma wiedzy o ich istnieniu. Jeśli decydujesz się na przenoszenie kodu, zastanów się, ile polegasz na tych funkcjach w projekcie. W kilku przypadkach przeprojektowanie jest zbyt drogie i chcesz trzymać się niezarządzanego kodu. Jest to pierwsza decyzja, którą należy podjąć, zanim zaczniesz hacking.

Zalety języka MC++ w języku C# lub Visual Basic

Pochodzący z niezarządzanego tła program MC++ zachowuje wiele możliwości obsługi niebezpiecznego kodu. Możliwość bezproblemowego łączenia zarządzanego i niezarządzanego kodu mc++zapewnia deweloperowi dużą moc i możesz wybrać miejsce na gradientzie, który chcesz usiąść podczas pisania kodu. Na jednej skrajności możesz napisać wszystko w prosty, nieuważny C++ i po prostu skompilować za pomocą /clr. Z drugiej strony można napisać wszystko jako obiekty zarządzane i radzić sobie z ograniczeniami języka i problemami z wydajnością wymienionymi powyżej.

Ale prawdziwa moc MC++ przychodzi, gdy wybierasz gdzieś między. Mc++ umożliwia dostosowanie niektórych trafień wydajności związanych z kodem zarządzanym, zapewniając dokładną kontrolę nad tym, kiedy używać niebezpiecznych funkcji. Język C# ma niektóre z tych funkcji w niebezpiecznym słowie kluczowym, ale nie jest integralną częścią języka i jest znacznie mniej przydatny niż MC++. Przyjrzyjmy się kilku przykładom pokazującym bardziej szczegółowość dostępną w programie MC++, a my omówimy sytuacje, w których przydaje się.

Uogólnione wskaźniki "byref"

W języku C# adres niektórych składowych klasy można pobrać tylko przez przekazanie go do parametru ref . W języku MC++wskaźnik byref jest konstrukcją pierwszej klasy. Adres elementu można pobrać w środku tablicy i zwrócić ten adres z funkcji:

Byte* AddrInArray( Byte b[] ) {
   return &b[5];
}

Wykorzystujemy tę funkcję do zwracania wskaźnika do "znaków" w ciągu System.String za pośrednictwem procedury pomocnika, a nawet możemy pętli przez tablice przy użyciu tych wskaźników:

System::Char* PtrToStringChars(System::String*);   
for( Char*pC = PtrToStringChars(S"boo");
  pC != NULL;
  pC++ )
{
      ... *pC ...
}

Możesz również wykonać przechodzenie połączonej listy za pomocą iniekcji w MC++ przez pobranie adresu pola "dalej" (którego nie można wykonać w języku C#):

Node **w = &Head;
while(true) {
  if( *w == 0 || val < (*w)->val ) {
    Node *t = new Node(val,*w);
    *w = t;
    break;
  }
  w = &(*w)->next;
}

W języku C#nie można wskazać "Head" lub podjąć adresu pola "next", dlatego w pierwszej lokalizacji wstawiasz specjalny przypadek lub jeśli wartość "Head" ma wartość null. Co więcej, musisz spojrzeć na jeden węzeł do przodu przez cały czas w kodzie. Porównaj to z dobrymi produktami w języku C#:

if( Head==null || val < Head.val ) {
  Node t = new Node(val,Head);
  Head = t;
}else{
  // we know at least one node exists,
  // so we can look 1 node ahead
  Node w=Head;
while(true) {
  if( w.next == null || val < w.next.val ){
    Node t = new Node(val,w.next.next);
    w.next = t;
    break;
  }
  w = w.next;
  }
}         

Dostęp użytkownika do typów boxed

Problem z wydajnością typowy dla języków OO to czas spędzony na boksie i rozpatkowaniu wartości. Mc++ zapewnia o wiele większą kontrolę nad tym zachowaniem, więc nie trzeba dynamicznie (ani statycznie) rozpakować wartości dostępu do wartości. Jest to kolejna ulepszenia wydajności. Wystarczy umieścić __box słowo kluczowe przed dowolnym typem, aby reprezentować jego formularz w polu:

__value struct V {
  int i;
};
int main() {
  V v = {10};
  __box V *pbV = __box(v);
  pbV->i += 10;           // update without casting
}

W języku C# musisz cofnąć pole wyboru do elementu "v", a następnie zaktualizować wartość i ponownie zaznaczyć pole z powrotem do obiektu:

struct B { public int i; }
static void Main() {
  B b = new B();
  b.i = 5;
  object o = b;         // implicit box
  B b2 = (B)o;            // explicit unbox
  b2.i++;               // update
  o = b2;               // implicit re-box
}

Kolekcje STL a kolekcje zarządzane — wersja 1

Zła wiadomość: W języku C++używanie kolekcji STL było często tak szybkie, jak pisanie tej funkcji ręcznie. Struktury CLR są bardzo szybkie, ale cierpią z powodu problemów z boksem i rozpatkowaniem: wszystko jest obiektem i bez szablonu lub ogólnej obsługi, wszystkie akcje muszą być sprawdzane w czasie wykonywania.

Dobra wiadomość: W dłuższej perspektywie można założyć, że ten problem zniknie, ponieważ typy ogólne są dodawane do czasu wykonywania. Kod, który wdrożysz dzisiaj, będzie doświadczał zwiększenia szybkości bez żadnych zmian. W krótkim okresie można użyć rzutowania statycznego, aby zapobiec sprawdzaniu, ale nie jest to już bezpieczne. Zalecam użycie tej metody w ścisłym kodzie, w którym wydajność jest absolutnie krytyczna i zidentyfikowano dwa lub trzy gorące punkty.

Korzystanie z obiektów zarządzanych stosu

W języku C++określisz, że obiekt powinien być zarządzany przez stos lub stertę. Nadal możesz to zrobić w programie MC++, ale należy pamiętać o ograniczeniach. ClR używa wartości ValueTypes dla wszystkich obiektów zarządzanych przez stos i istnieją ograniczenia dotyczące tego, co można zrobić valueTypes (na przykład bez dziedziczenia). Więcej informacji jest dostępnych w bibliotece MSDN.

Przypadek narożny: uważaj na wywołania pośrednie w kodzie zarządzanym — wersja 1

W czasie wykonywania w wersji 1 wszystkie wywołania funkcji pośrednich są wykonywane natywnie i w związku z tym wymagają przejścia do niezarządzanego miejsca. Dowolne wywołanie funkcji pośredniej można wykonać tylko z trybu natywnego, co oznacza, że wszystkie wywołania pośrednie z kodu zarządzanego wymagają przejścia zarządzanego do niezarządzanego. Jest to poważny problem, gdy tabela zwraca funkcję zarządzaną, ponieważ należy wykonać drugie przejście w celu wykonania funkcji. W porównaniu z kosztem wykonywania pojedynczej instrukcji Call koszt wynosi pięćdziesiąt do stu razy wolniej niż w języku C++!

Na szczęście podczas wywoływania metody, która znajduje się w klasie zbieranej przez śmieci, funkcja optymalizacji usuwa ten błąd. Jednak w konkretnym przypadku zwykłego pliku C++, który został skompilowany przy użyciu /clr, metoda zwracana zostanie uznana za zarządzaną. Ponieważ nie można go usunąć przez optymalizację, zostanie osiągnięty koszt pełnego podwójnego przejścia. Poniżej znajduje się przykład takiego przypadku.

//////////////////////// a.h:    //////////////////////////
class X {
public:
   void mf1();
   void mf2();
};

typedef void (X::*pMFunc_t)();


////////////// a.cpp: compiled with /clr  /////////////////
#include "a.h"

int main(){
   pMFunc_t pmf1 = &X::mf1;
   pMFunc_t pmf2 = &X::mf2;

   X *pX = new X();
   (pX->*pmf1)();
   (pX->*pmf2)();

   return 0;
}


////////////// b.cpp: compiled without /clr /////////////////
#include "a.h"

void X::mf1(){}


////////////// c.cpp: compiled with /clr ////////////////////
#include "a.h"
void X::mf2(){}

Istnieje kilka sposobów, aby tego uniknąć:

  • Utwórz klasę w klasie zarządzanej ("__gc")
  • Usuń wywołanie pośrednie, jeśli jest to możliwe
  • Pozostaw klasę skompilowana jako niezarządzany kod (np. nie używaj /clr)

Minimalizowanie trafień wydajności — wersja 1

Istnieje kilka operacji lub funkcji, które są po prostu droższe w programie MC++ w wersji 1 JIT. Wymienię je i wyjaśnię, a następnie porozmawiamy o tym, co możesz z nimi zrobić.

  • Abstrakcje — jest to obszar, w którym mocno wygrywa kompilator zaplecza języka C++. Jeśli zawijasz int wewnątrz klasy na potrzeby abstrakcji i uzyskujesz do niej dostęp ściśle jako liczba całkowitą, kompilator języka C++ może zmniejszyć obciążenie otoki praktycznie niczym. Do otoki można dodać wiele poziomów abstrakcji bez zwiększania kosztów. Tryb JIT nie może zająć czasu niezbędnego do wyeliminowania tego kosztu, dzięki czemu głębokie abstrakcje są droższe w programie MC++.
  • Zmiennoprzecinkowe — tryb JIT w wersji 1 nie wykonuje obecnie wszystkich optymalizacji specyficznych dla fp, które wykonuje zaplecze VC++, co sprawia, że operacje zmiennoprzecinkowe są na razie droższe.
  • Tablice wielowymiarowe — JIT jest lepiej obsługiwać tablice poszarpane niż tablice wielowymiarowe, więc zamiast tego należy używać tablic poszarpanych.
  • 64-bitowa arytmetyka — w przyszłych wersjach do trybu JIT zostaną dodane optymalizacje 64-bitowe.

Co można zrobić

W każdej fazie opracowywania istnieje kilka rzeczy, które można zrobić. W przypadku mc++, faza projektowania jest być może najważniejszym obszarem, ponieważ określi, ile pracy wykonujesz i ile wydajności otrzymasz w zamian. Gdy usiądziesz do zapisu lub portu aplikacji, należy wziąć pod uwagę następujące kwestie:

  • Identyfikowanie obszarów, w których używasz wielu dziedziczenia, szablonów lub finalizacji deterministycznej. Musisz pozbyć się tych elementów lub pozostawić tę część kodu w niezarządzanej przestrzeni. Zastanów się nad kosztem przeprojektowania i zidentyfikuj obszary, które można przenosić.
  • Zlokalizuj punkty aktywne wydajności, takie jak głębokie abstrakcje lub wywołania funkcji wirtualnych w przestrzeni zarządzanej. Będą one również wymagać decyzji projektowej.
  • Wyszukaj obiekty, które zostały określone jako zarządzane przez stos. Upewnij się, że można je przekonwertować na wartość ValueTypes. Oznacz inne obiekty do konwersji na obiekty zarządzane przez stertę.

Na etapie kodowania należy pamiętać o operacjach, które są droższe, oraz o opcjach, które masz do czynienia z nimi. Jedną z najładniejszych rzeczy dotyczących mc++ jest to, że masz do ściągnąć wszystkie problemy z wydajnością z góry, zanim zaczniesz kodować: jest to pomocne podczas analizowania pracy później. Jednak nadal istnieją pewne poprawki, które można wykonać podczas kodzie i debugowania.

Ustal, które obszary intensywnie korzystają z arytmetycznych, wielowymiarowych tablic lub funkcji biblioteki. Które z tych obszarów mają krytyczne znaczenie dla wydajności? Użyj profilatorów, aby wybrać fragmenty, w których obciążenie jest najbardziej kosztowne, i wybierz, która opcja wydaje się najlepsza:

  • Zachowaj cały fragment w niezarządzanej przestrzeni.
  • Użyj rzutów statycznych w dostępie do biblioteki.
  • Spróbuj dostosować zachowanie boxing/unboxing (wyjaśnione później).
  • Tworzenie kodu własnej struktury.

Na koniec zminimalizuj liczbę wykonanych przejść. Jeśli masz niezarządzany kod lub wywołanie międzyoperace siedzące w pętli, utwórz całą pętlę niezarządzaną. W ten sposób zapłacisz tylko dwa razy koszt przejścia, a nie za każdą iterację pętli.

Dodatkowe zasoby

Powiązane tematy dotyczące wydajności w .NET Framework obejmują:

Obejrzyj przyszłe artykuły obecnie opracowywane, w tym omówienie filozofii projektowania, architektury i kodowania, przewodnik po narzędziach do analizy wydajności w świecie zarządzanym oraz porównanie wydajności platformy .NET z innymi dostępnymi obecnie aplikacjami dla przedsiębiorstw.

Dodatek: Koszt wywołań wirtualnych i alokacji

Typ wywołania Liczba wywołań na sekundę
ValueType niewirtualne wywołanie 809971805.600
Wywołanie niewirtualne klasy 268478412.546
Wywołanie wirtualne klasy 109117738.369
ValueType Virtual (Obj, metoda) Call 3004286.205
ValueType Virtual (Overridden Obj Method) Call 2917140.844
Typ ładowania według nowych (niestatyczny) 1434.720
Typ ładowania według nowych (metod wirtualnych) 1369.863

Uwaga Maszyna testowa to PIII 733Mhz z systemem Windows 2000 Professional z dodatkiem Service Pack 2.

Ten wykres porównuje koszt skojarzony z różnymi typami wywołań metod, a także kosztem utworzenia wystąpienia typu zawierającego metody wirtualne. Im większa liczba, tym więcej wywołań/wystąpień na sekundę można wykonać. Chociaż te numery z pewnością będą się różnić na różnych maszynach i konfiguracjach, względny koszt wykonywania jednego wywołania przez inne pozostaje znaczący.

  • ValueType Non-Virtual Call: Ten test wywołuje pustą metodę niewirtuacyjną zawartą w elemecie ValueType.
  • Wywołanie niewirtualne klasy: ten test wywołuje pustą metodę niewirtuacyjną zawartą w klasie.
  • Wywołanie wirtualne klasy: ten test wywołuje pustą metodę wirtualną zawartą w klasie.
  • Wywołanie metody ValueType Virtual (Obj Method): ten test wywołuje metodę ToString() (metoda wirtualna) w metodzie ValueType, która jest stosowana do domyślnej metody obiektu.
  • Wywołanie metody ValueType Virtual (Overridden Obj Method): ten test wywołuje metodę ToString() (metoda wirtualna) dla wartości ValueType, która zastąpiła wartość domyślną.
  • Typ obciążenia według nowych (statycznych): ten test przydziela miejsce dla klasy tylko za pomocą metod statycznych.
  • Typ obciążenia według nowych (metod wirtualnych): ten test przydziela miejsce dla klasy za pomocą metod wirtualnych.

Jednym z wniosków, które można wyciągnąć, jest to, że wywołania funkcji wirtualnej są około dwa razy droższe niż zwykłe wywołania podczas wywoływania metody w klasie. Należy pamiętać, że połączenia są tanie na początek, więc nie usuwałbym wszystkich wirtualnych wywołań. Zawsze należy używać metod wirtualnych, gdy ma to sens.

  • Tryb JIT nie może wbudowanych metod wirtualnych, więc utracisz potencjalną optymalizację, jeśli pozbysz się metod niewirtualnych.
  • Przydzielanie miejsca dla obiektu, który ma metody wirtualne, jest nieco wolniejsze niż alokacja dla obiektu bez nich, ponieważ należy wykonać dodatkową pracę, aby znaleźć miejsce dla tabel wirtualnych.

Zauważ, że wywoływanie metody innej niż wirtualna w klasie ValueType jest więcej niż trzy razy szybciej niż w klasie, ale po traktowaniu jej jako klasy tracisz strasznie. Jest to cecha valueTypes: traktuj je jak struktury i są one szybko oświetlone. Traktuj je jak klasy i są boleśnie powolne. ToString() jest metodą wirtualną, więc zanim będzie można ją wywołać, struktura musi zostać przekonwertowana na obiekt na stercie. Zamiast być dwa razy wolnego, wywoływanie metody wirtualnej w typie ValueType jest teraz osiemnaście razy wolne! Moralność historii? Nie traktuj wartości ValueTypes jako klas.

Jeśli masz pytania lub komentarze dotyczące tego artykułu, skontaktuj się z Claudio Caldato, menedżerem programu w celu .NET Framework problemów z wydajnością.