BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Bezpośrednie operacje na pamięci w C++/CLI cz.2

30-04-2007 23:08 | uzytkownik usuniety
Artykuł stanowi kontynuację części pierwszej i traktuje o pinning pointers. Pokazuje jakie są zalety tego rozwiązania oraz zwraca uwagę na wady. Jest to ostatni artykuł z serii operacji na pamięci w C++/CLI. Zapraszam, i życzę miłej lektury :)

Pinning pointers


Jak pisałem już w innych artykułach, mechanizm GC przesuwa podczas swoich cykli wszystkie dane zapisane na zarządzanej stercie. Wskaźniki natywne nie współpracują z obiektami CLI, z powodów o których pisałem w części pierwszej tego artykułu. Również z powodów o których pisałem wcześniej powstały podwójne wskaźniki (interior pointers), które same dbają o to, żeby pokazywać na właściwy obiekt niezależnie od tego w którym miejscu na zarządzanej stercie się obecnie znajduje. Jest to bardzo dobre rozwiązanie gdy pracujemy tylko na zarządzanych obiektach, jednak co jeśli chcemy mieć możliwość „współpracy” także z niezarządzanym kodem? Przykładowo, gdybyśmy chcieli przekazać do natywnej funkcji podwójny wskaźnik, nie umiałaby ona przekonwertować go do wskaźnika natywnego. I tu właśnie do gry wchodzą pinning pointers.

Wskaźniki te „przypinają się” do obiektu zarządzanego. Obiekt taki pozostaje pod opieką pinning pointer tak długo, aż ten jest żywy – tj. dopóki nie wyjdzie z zakresu. GC wiedząc o tym, że obiekt jest przypięty nie realokuje go. Ci, którzy znają kaskadowe arkusze stylów (CSS) mogą porównać sobie ten rodzaj wskazania do wartości fixed właściwości position. W miarę działania programu (czyli w naszym przykładzie przewijanie stronki) obiekty pokazywane przez pinning pointers się nie przesuwają w pamięci (elementy z własnością position: fixed).

Teraz gdy już mamy zagwarantowane, że obiekt niespodziewanie nie przesunie nam sie w pamięci, kompilator pozwoli na konwersję z pinning pointer do zwykłego wskaźnika i co za tym idzie, pozwoli nam użyć takiego wskaźnika w kodzie niezarządzanym. Słowo „przypięte” jest tutaj dobrym opisem całego mechanizmu. Wystarczy wyobrazić sobie wskaźniki na stałe przypięte do adresu w pamięci.

Składnia jest podobna do tej znanej z podwójnych wskaźników:

pin_ptr< typ > var = [adres];

Jak wcześniej wspominałem czas przypięcia przez wskaźnik odpowiada czasowi, do chwili gdy nie wyjdzie on spoza zakresu. Jeśli wartość pinning pointer zmieni się na nullptr, wówczas jest to równoznaczne z odpięciem obiektu, który wraca pod władzę GC. Natomiast jeśli pod wskaźnik przypiszemy adres innego obiektu stary obiekt zostanie odpięty, a na jego miejsce zostanie przypięty nowy.

Kod pokazuje różnice pomiędzy podwójnymi wskaźnikami a pininig pointer. Pętle for zostały użyte, żeby wypełnić zarządzaną stertę oraz aby do akcji wkroczył GC:

[Kod C++]

#include <cstdio>

using namespace System;
using
namespace std;

ref class A
{
public:
    int a;
};

int main(array<System::String ^> ^args)
{
    for(int i=0; i<100000; i++)
        gcnew A(); // Wypełnienie zarządzanej sterty
   

    A^ d1 = gcnew A(); // ((1))

    for(int i=0; i<1000; i++)
        gcnew A();

    A^ d2 = gcnew A();

    interior_ptr<int> intptr = &d1->a; // ((2))
    pin_ptr<int> pinptr = &d2->a; // ((3))

    printf("intptr=%p pinptr=%p\r\n", intptr, pinptr); // Wypisanie adresów wskaźników przed GC

    for(int i=0; i<100000; i++) // ((4))
        gcnew A();

    printf("intptr=%p pinptr=%p\r\n", intptr, pinptr); // Wypisanie adresów wskaźników po GC

}


W programie tworzymy dwa obiekty ((1)) typu A oraz dziurę w pamięci między nimi. Następnie do podwójnego wskaźnika ((2)) oraz pinning pointer ((3)) przypisujemy adres pola a każdego z obiektów. Dzięki stworzeniu wielu instancji klasy A zmuszamy do działania nasz garbage-collecor (uwaga: wywołując metodę GC::Colloect także można zmusić go do działania). Na wyjściu dostaniemy coś na kształt:

intptr=01314218 pinptr=01317104
intptr=0130181C pinptr=01317104


Oczywiście adresy pamięci będą się różnić. Ważne jest to, że adres pinning pointer jest cały czas ten sam, co potwierdza fakt który starałem się wyjaśnić wcześniej. Użycie pinning pointer informuje GC o tym, że obiekt jest przypięty. GC zostawia takie obiekty w spokoju nie przesuwając ich, dzięki czemu możemy przekazać taki wskaźnik do kodu niezarządzanego.


Przekazywanie do natywnego kodu


Dzięki temu, że obiekty nie są przesuwane, kompilator może dokonać niejawnej konwersji z a do wskaźnika natywnego. Idąc dalej możemy przekazać taki wskaźnik do funkcji natywnej oczekującej wskaźnika tego samego typu. Pamiętacie zapewne, że w C++ może być wykonana tylko jedna niejawna konwersja, dlatego gdyby nasza funkcja oczekiwała jakiegoś innego typu, nie udałoby się skompilować takiego kodu. Spójrzmy na poniższy kawałek kodu – definicję funkcji:

[Kod C++]

#pragma unmanaged
int
NativeCountVowels(wchar_t* pString)
{
    int count = 0;

    const wchar_t* vowarr = L"aeiouAEIOU";

    while(*pString)
        if(wcschr(vowarr,*pString++))
            count++;

    return count;
}
#pragma managed




#pragma managed/unmanaged

Dyrektywy te pomagają nam skompilować kod zgodnie z naszym zamierzeniem. Gdy chcemy skompilować funkcję jako niezarządzaną, to pisząc #pragma unmanaged, gwarantujemy sobie, że ten kawałek kodu zostanie skompilowany jako natywny i nie będzie używał CLR. Jeśli wyspecyfikujemy kod jako #pragma managed, zostanie wygenerowany MSIL (o tym metajęzyku napiszę w jednym z kolejnych artykułów) a kod zostanie uruchomiony przy użyciu CLR. Gdy używamy tych dyrektyw pamiętajmy aby następnie napisać dyrektywę odwołującą.


A w ten sposób możemy wywołać powyższą funkcję. Najpierw tworzymy pinning pointer na pierwszy znak napisu a następnie przekazujemy go do naszej funkcji:

String^ s = "Wiekszosc ludzi nie wiem ze CLR jest napisane w C++";
pin_ptr
<Char> p = const_cast< interior_ptr<Char> >( PtrToStringChars(s) );
Console::WriteLine(NativeCountVowels(p));


O funkcji PtrToStringChars pisałem już wcześniej, dlatego teraz tylko przypomnę, że na podstawie argumentu typu zarządzanego String zwraca nam stały wskaźnik (podwójny wskaźnik). Druga linijka powyższego kodu wygląda naprawdę strasznie, ale już tłumaczę. Najpierw przy pomocy powyższej funkcji wyłuskujemy podwójny wskaźnik na pierwszy element. Ponieważ, jest on opatrzony modyfikatorem const, usuwamy go przy pomocy rzutowania const_cast na podwójny wskaźnik do Char. Od tej pory mamy wskaźnik przy pomocy którego oprócz przeglądania kolejnych znaków naszego napisu także je zmodyfikujemy. W następnej linijce widzimy jak łatwo dzięki C++/CLI jest przejść z dwóch światów – z zarządzanego do niezarządzanego.

1.JPG

Na obrazku widzimy konwersje możliwe między wskaźnikami w C++/CLI. Jako jedyna, nie jest możliwa konwersja w ze wskaźników podwójnych wskaźników na natywne. Każda inna konwersja nie tylko jest możliwa ale także zachodzi niejawnie.

Pamiętajcie jednak, że mechanizmu o którym mowa nie należy nadużywać (jak zresztą każdego mechanizmu). Zbytnie nadużywanie pinning pointer prowadzi do problemu fragmentacji sterty.


Problem fragmentacji sterty


Poprzednie rozważania były bardziej praktyczne. Teraz czas na trochę teorii, która pokaże nam że mechanizmu trzeba „używać z głową”. Otóż na stercie CLR obiekty są zawsze alokowane sekwencyjnie. Przy każdym cyku GC, mechanizm kompaktuje stertę, dzięki czemu jest ona spójna, a dane są upakowane tak, aby nie było żadnych luk i tym samym utraty puli pamięci do przydzielenia. Spójrzmy na poniższy obrazek. Widzimy, że kolejne obiekty na stercie są zaalokowane „jeden po drugim”.

2.JPG

Przed cyklem GC

Na stercie mamy trzy obiekty. Jednak obiekt nr 2 nie jest już używany, więc w następnym cyku zostanie skasowany.

3.JPG

Po cyklu GC

Nie używany obiekt został usunięty, a reszta danych zkompatkowana, dlatego pierwszy i trzeci obiekt są teraz razem. Ideą tego wszystkiego jest doprowadzenie do stanu gdy największa ilość pamięci jest wolna a zaalokowana pamięć tworzyła spójny blok na stercie.


4.JPG

Po przypięciu Obj3

Załóżmy teraz, że Obj3 jest pokazywany przez pinning pointer. Skro GC nie przesuwa danych pokazywanych przez takie wskaźniki, Obj3 po cyklu zostaje dalej na swoim miejscu. Nie można przez to utworzyć spójnego bloku na stercie, a w miejscu Obj2 zostaje przerwa. Wystarczy sobie wyobrazić, co by było gdyby kilka(set) takich obiektów znajdowało się jednocześnie na stercie. Zamiast niej mielibyśmy ser szwajcarski. Najgorsze w tym wszystkim jest to, że mimo, że pamięć jest wolna to nie możemy jej użyć, bo jak zapewne pamiętacie tablice, czy duże obiekty nie mogą być w połowie przerwane i podzielone na dwie lub więcej części. Poza tym dostęp do pamięci zabiera więcej czasu niż zwykle, ponieważ GC stara się znaleźć odpowiednio duży, wolny blok pamięci, aby zmieścić obiekt.

5.JPG

W przypadku dużej ilości „przypiętych obiektów”

Pamiętajmy więc, żeby nie nadużywać pinning pointers.



Kilka rad przy używaniu pinning pointers:


Widzieliśmy już, kiedy wskaźniki są użyteczne, a kiedy mogą nam sprawić problem. Spróbujmy zebrać wszystko razem:

  • Dopóki naprawdę nie zajdzie taka potrzeba, nie używaj pinning pointers. Najpierw zastanów się, czy problemu nie rozwiąże podwójny wskaźnik lub śledząca referencja. Jeśli tylko jest jakaś alternatywa, skorzystaj z niej.

  • Jeśli potrzebujesz przypiąć parę obiektów, staraj się zaalokować je razem, żeby mogły być jak najbliżej siebie na stercie. Pomoże to uniknąć problemów o których pisałem wyżej, bo GC zaalokuje możliwą pamięć zarówno przed jak i za nimi.

  • Jeśli odwołujesz się do natywnego kodu, sprawdź czy warstwa pośrednia CLR (lub kod natywny) nie dokonały przypięcia za Ciebie. Jeśli tak, nie trzeba już drugi raz tworzyć wskaźnika, żeby przekazać obiekt do kodu niezarządzanego. Dodatkowo piszemy tu niepotrzebny kod, który nie wprowadza niczego nowego.

  • Nowo zaalokowane obiekty podlegają pod zerową generację GC. Wiemy, że obiekty takie są najczęściej przesuwane. Nie powinniśmy zatem często przypinać alokowanych obiektów, gdyż możemy spowodować dużą fragmentację sterty (już w zerowej generacji)

  • Redukuj czas życia pinning pointers do minimum. Im dłużej taki wskaźnik pozostaje w zakresie, tym większe jest prawdopodobieństwo, że spowoduje on fragmentację pamięci. Jeśli potrzebujemy takiego wskaźnika wewnątrz bloku if, zadeklarujmy go właśnie tam, zamiast na zewnątrz.

  • Za każdym razem gdy przekształcamy pinning pointer do natywnego wskaźnika musimy być pewni, że gdy używamy natywnego wskaźnika to pinning pointer istnieje. Jeśli wyjdzie on poza zakres, obiekt jest odpinany i wraca pod władzę GC który może zrobić co mu się podoba (zazwyczaj zrobi tak, że program się wysypie :P). Takie błędy są bardzo trudne do wyłapania, nawet gdy posługujemy się debuggerem. Możemy myśleć, że jest to jakiś dziwny zbieg okoliczności, podczas gdy okaże się, że natywna funkcja przyjęła sobie jakiś wskaźnik i zaczęła pracować z opóźnieniem. Mogła też przekazać wynik w postaci takiego wskazania do jakiejś innej funkcji. Scenariusze można mnożyć ale i tak wszystkie prowadzą do jednego wniosku – rezultat może być fatalny. Najlepszym sposobem na uniknięcie tego typu problemów jest zorientowanie się do jakiego kodu przekazywany jest pinning pointer zanim go tam tak naprawdę przekażemy.

Była to ostatnia rada. Mam nadzieję, że przedstawiłem dość jasno mechanizm pinning pointers i wypadł on dobrze w oczach czytelników. Głównym celem tego artykułu było pokazanie dość specjalistycznego mechanizmu który pozwala nam połączyć dwa zupełnie różne światy, jakimi są kod zarządzany i niezarządzany. Daje nam on sporo korzyści i ułatwień, jednak nie ma nic za darmo. Pamiętajmy o konsekwencjach używania tego mechanizmu. Im bardziej świadomie będziemy projektować nasze programy tym bardziej doskonałe się staną. Tym pozytywnym akcentem kończę, drugą a zarazem ostatnią część artykułu „Bezpośrednie operacje na pamięci w C++/CLI”.

tagi: C++/CLI

Komentarze 6

radakon
radakon
1 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Odnoszę niemiłe wrażenie, że już gdzieś widziałem coś praktycznie identycznego (http://www.codeguru.pl/article-407.aspx).
A tak na marginesie, to w kontekście kontrolek z VS2005, artykuł ten wydaje się być mocno nieaktualny....

radzimskik
radzimskik
1 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Wyglądało na to że VS 2005 kończy temat wprowadzając ToolStrip-y i MenuStrip. Po tym artykule spodziewałem się opisu Customiz~acji i tego co jest w menu Office 2003 a nie ma w menustrip... Może nastepnym razem:)
DonDoman
DonDoman
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Plagiat to jedna rzecz (nie ma co do tego zadnych watpliwosci). Druga, o wiele gorsza sprawa to szwankujacy system weryfikacji zglaszanych publikacji. Czy cos takiego w ogole istnieje? Generalnie czytelnik publikujacy kolejny artykul nie koniecznie musi wiedziec, co juz zostalo opublikowane (pech chcial, ze w tym przypadku pomysl byl identyczny). Co innego w przypadku CodeGuru, ktore raczej dokładnie powinno znac wlasna baze publikacji ...
User 114134
User 114134
2 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
plagiat + nieaktualne treści, ocena: 1
kazikkuta VIP
kazikkuta
840 pkt.
Senior
21-01-2010
oceń pozytywnie 0
Niestety plagiat. Nawet przez chwilę przeszło mi przez głowę, że być może obydwaj autorzy skopiowali czyjaś pracę (inaczej ktoś okazałby się kompletnym idiotą myśląc, że taki numer przejdzie). Ale wspólne fragmenty pracy (fragmenty kodu, zastosowane zwroty) występują tylko na CodeGuru.pl i nigdzie indziej.
Co do systemu weryfikacji, to według mnie artykułów nie można odrzucać tylko dla tego, że są na temat już poruszony. Nowy artykuł może rzucić nowe światło na dany temat. Źle się stało, że plagiat nie został wykryty przed publikacją. Z drugiej, kto może powiedzieć, że zna na pamięć wszystkie (a jest ich już ponad 500) artykuły?
DonDoman
DonDoman
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Zgoda - podobny, czy ten sam tytuł nie powinnien stanowić o odrzuceniu artykułu (btw. w jednej z wcześniejszych edycjii codeguru sam popełniłem artykuł na temat już poruszony i został on bardzo pozytywnie oceniony - 1miejsce :) ), aczkolwiek przed jego opublikowaniem wypadałoby porównać go z wcześniejszymi wersjami. Tych nie powinno już być 500, a najczęściej pewnie tylko 1 :) Cóż, nauczka na przyszłość, rzec się chce :)
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.