Czy jest referencja zmiennych w Pascalu?

0

Chcę obliczać i zwracać dane typu TPoint lub TRect, czyli rekordy.

function rotPt(x,y, s,c : integer) : TPoint;
begin
  result.x := x*c + y*s;
  result.y := x*s - y*c;
end;  


procedure test(var p : TPoint; a : single);
var pt : TPoint;
begin
      s := sin(a); c := cos(a);
      
      pt := rotPt(p.x,p.y,s,c); // taki wariant - na lokalnej zmiennej

      p := rotPt(p.x,p.y,s,c); // albo taki
end;

I tu jakieś głupoty kompilator generuje.

On robi to tak:

  1. tworzy na stosie tmp : TPoint
  2. podaje adres do tego tmp dla rotPt
  3. w rotPt result jest faktycznie typ tmp
  4. po obliczeni kopiuje ten tmp do pt, czy p

Przecież to jest chore.
Powinien chyba zapodać od razu adres do tego docelowego rekordu - pt, a nie do tmp,
i wtedy nie ma potrzeby tego kopiować, i stos marnować na pierdoły.

I to byłoby zwracanie referencji, czyli taki niejawny wskaźnik.
Można zwracać wskaźnik... ale wtedy jest do d**y.

Przy takim wywołaniu może sobie tworzyć temps:

with rotPt(66,88, pi/17) do ...
1

To nie ma nic związanego z językiem (Pascalem), tylko z implementacją konkretnego kompilatora i jego możliwościami optymalizacji.
Mechanizm nazywa się „return value optimization”, i najwyraźniej go zabrakło.
Nie podałeś jaki to kompilator. Jeśli Turbo Pascal, to w nim optymalizacja praktycznie nie istnieje.

0

Żeby to usprawnić można napisać tak:

procedure rotPt(var Point: TPoint, x, y, s,c : integer);
begin
  Point.x := x*c + y*s;
  Point.y := x*s - y*c;
end;  

Obliczenia będą robione na Point stworzonym zanim została wywołana funkcja.
Innym sposobem żeby ominąć kopiowanie tego rekordu jest zwrócenie wskaźnika na rekord jako rezultat, ale to już wiesz...

0

W tej wersji nie mogę robić tak:
with rotPt(...) do dosomething(x,y);

A może zrobić to w wersji inline?

function rotPt(var Point: TPoint, x, y, s,c : integer); TPoint; inline;
begin
  Point.x := x*c + y*s;
  Point.y := x*s - y*c;
  rotPt := Point;
end;  

Trzeba sprawdzić co z tego wyjdzie.
Ostatecznie pozostanie wskaźnik PPoint i wówczas od biedy można robić tak:

with rotPt(...)^ do dosomething(x,y);

Paskal jest i tak do kitu w tych sprawach.
A może na funkcjach w obiekcie w TPoint lepiej wyjdzie.

p := MakePoint(x,y).rot(angle).

można coś takiego zrobić w paskalu?
Ale tu znowu będzie tworzył tmp i potem kopiował... do kitu te komplilatorki.

Przecież łatwo rozpoznawać takie sprawy i liczyć od razu na p.
Kompilatorów nie potrafią robić... chyba musiałbym zrobić porządny kompilator od zera.

0
herlack napisał(a):

Ostatecznie pozostanie wskaźnik PPoint i wówczas od biedy można robić tak:

with rotPt(...)^ do dosomething(x,y);

W przypadku składni Delphi jawna dereferencja - z tego co pamiętam - nie jest potrzebna.

A może na funkcjach w obiekcie w TPoint lepiej wyjdzie.

p := MakePoint(x,y).rot(angle).

można coś takiego zrobić w paskalu?

Dlaczego miałoby się nie dać?
Osobiście sugerowałbym advanced record, lecz wyjdzie na jedno (jedynie w przypadku korzystania z rekordów masz internalsowo zaimplementowany prosty GC (refcounting) i nie musisz się martwić o memleaki, jak w przypadku obiektów).

Przecież łatwo rozpoznawać takie sprawy i liczyć od razu na p.

Tobie łatwo, kompilatorowi niekoniecznie...

Kompilatorów nie potrafią robić... chyba musiałbym zrobić porządny kompilator od zera.

FPC jest open-source, dołącz się i zaimplementuj tę optymalizację - każdy będzie zadowolony. Sam zresztą powiedziałeś, że jest to łatwe.


Ciekawostka: `RVO` zostało zaimplementowane w GCC 3.1 (2002 rok - 15 lat po wydaniu pierwszej wersji; o ile się nie mylę, to FPC nawet nie ma 15 lat, zatem jeszcze mają czas ;]).
0

Może zrobiłbym, ale ja w paskalu robię tylko tak z lenistwa; głównie jadę na c++.

Ale zauważyłem że w c++ jest tak samo z tym kopiowaniem poprzez temp,
a poza tym w C++ brakuje with, co często bardzo upraszcza kod, no i kompilatorowi ułatwia robotę,
i zwykle wtedy generuje znacznie lepszy kod, bo oblicza i ładuje adres tego rekordu tylko raz.

No, ale widzę że straszliwie obrzydliwy kod generują te obiektowe kompilatory,
a zatem to muszą tworzyć kompletnie zieleni programiści, więc raczej nie mógłbym z nimi współpracować;
nie będę przecież ich uczył elementarza, bo wtedy szybciutko obraziliby się i na tym zakończyłaby się ta 'współpraca'.

0

No, ale widzę że straszliwie obrzydliwy kod generują te obiektowe kompilatory

Czekaj, czekaj - twierdzisz, że np.potężne GCC generuje obrzydliwy kod?

a zatem to muszą tworzyć kompletnie zieleni programiści, więc raczej nie mógłbym z nimi współpracować;

Z tego co pamiętam, to GCC tworzą głównie inżynierzy oraz ludzie o innych interesujących tytułach naukowych, i to nie byle jacy...


i zwykle wtedy generuje znacznie lepszy kod, bo oblicza i ładuje adres tego rekordu tylko raz.

To się nazywa CSE, i nawet FPC posiada tę optymalizację (chociaż działa tam dosyć słabo z tego co sprawdzałem, niestety).

0
Patryk27 napisał(a):

Czekaj, czekaj - twierdzisz, że np.potężne GCC generuje obrzydliwy kod?

Tego nie sprawdzałem, ale to pewnie jakiś kolejny standard, tradycja, więc powinno być tak samo.

To się nazywa CSE, i nawet FPC posiada tę optymalizację (chociaż działa tam dosyć słabo z tego co sprawdzałem, niestety).

U mnie to wcale nie działa.

if set then T[i,j] := T[i,j] or FLAGA
else          T[i,j] := T[i,j] and not FLAGA;

I tu kompilator oblicza 4 razy element T[i,j], i do tego mnoży tam za każdym razem:
i*rozmiar linii + j, i nawet w pętli jakby nie wiedział, że jedzie kolejno -
jak po tablicy jednowymiarowej, czyli w zasadzie nie ma czego tu obliczać.

0

Włączyłeś -OoCSE?
+która wersja FPC?

Edit: CSE działa chyba tak czy siak jedynie na poziomie stricte kodu wejściowego; to, że na wyjściu będzie i*rozmiar linii+j raczej nie ma niestety tutaj znaczenia. Natomiast może się mylę, nie patrzałem do źródeł FPC (pod tym kątem) i nie sprawdzałem outputu assemblera.

0

Ale zauważyłem że w c++ jest tak samo z tym kopiowaniem poprzez temp,
Wcale nie jest tak samo.
Ale optymalizację trzeba włączyć: pod GCC parametr -O2 albo -O3 (O jak Okrzesław, nie zero), pod MSVC++ parametr -Ox albo wybrać kompilację Release w IDE.

PS. w końcu ktoś poprawił topic...

0
Azarien napisał(a):

Wcale nie jest tak samo.

Wątpię. Zrób asm wtedy zobaczymy.

Tam jest strasznie gdy używasz wprost te obiektowe operatory - jak w matematyce.
Np. obliczenia na complex:

complex a(1,4), b(3), i(0,1), c, d;

c = a + b - i;

tu otrzymasz chyba dwa kopiowania, albo i trzy - dla każdego operatora.

Przy dłuższych obliczeniach tego typu kompilator rezerwuje pełno tymczasowych zmiennych na stosie - dziesiątki!

d = a^2 * 2 + b*c - c^2;
d = sqrt(d) - 1 + log(c);

normalnie masakra z tego wyjdzie.

Gdy używamy operacji 'na obiekcie', wtedy jest lepiej, np.:

a += b; // zamiast: a = a + b;
a = b; a.sqrt(); // zamiast: a = sqrt(b);

tu nie będzie tworzył tych tmps.

0
herlack napisał(a)

tu otrzymasz chyba dwa kopiowania, albo i trzy - dla każdego operatora.

Nieprawda - akurat w tym wypadku GCC policzy wynik już w czasie kompilacji :P

Gdy używamy operacji 'na obiekcie', wtedy jest lepiej, np.:

a += b; // zamiast: a = a + b;
#include <iostream>
#include <complex>

typedef std::complex<int> comp;

int main()
{
 comp a, b;

 std::cin >> a >> b;

 a = a+b;

 return real(a);
}

g++ cpp.cpp -masm=intel -O2 -fverbose-asm -S produkuje:

mov	eax, DWORD PTR [esp+16]	 # tmp66, a._M_real
add	eax, DWORD PTR [esp+24]	 # tmp66, MEM[(const int &)&b]

Zamiana na a += b zwraca dokładnie to samo.


Przy dłuższych obliczeniach tego typu kompilator rezerwuje pełno tymczasowych zmiennych na stosie - dziesiątki!

d = a^2 * 2 + b*c - c^2;
d = sqrt(d) - 1 + log(c);

normalnie masakra z tego wyjdzie.

#include <iostream>
#include <complex>

int main()
{
 std::complex<int> a, b, c, d;

 std::cin >> a >> b >> c >> d;

 d = a*a * 2 + b*c - c*c;
 d = sqrt(d) - 1 + log(c);

 return real(d);
}

Zwraca

	mov	esi, DWORD PTR [esp+64]	 # __r$_M_real, c._M_real
	mov	ebx, DWORD PTR [esp+68]	 # __r$_M_imag, c._M_imag
	mov	eax, esi	 #, __r$_M_real
	imul	eax, ebx	 #, __r$_M_imag
	mov	DWORD PTR [esp+36], eax	 # %sfp,
	mov	edx, DWORD PTR [esp+48]	 # __r$_M_real, a._M_real
	mov	ecx, DWORD PTR [esp+52]	 # D.24090, a._M_imag
	imul	ecx, edx	 # D.24090, __r$_M_real
	mov	eax, DWORD PTR [esp+56]	 # tmp109, b._M_real
	imul	eax, esi	 # tmp109, __r$_M_real
	mov	edi, DWORD PTR [esp+60]	 # tmp108, b._M_imag
	imul	edi, ebx	 # tmp108, __r$_M_imag
	sub	eax, edi	 # tmp109, tmp108
	imul	edx, edx	 # tmp112, __r$_M_real
	mov	edi, DWORD PTR [esp+52]	 # tmp111, a._M_imag
	imul	edi, edi	 # tmp111, tmp111
	sub	edx, edi	 # tmp112, tmp111
	lea	eax, [eax+edx*2]	 # tmp118,
	mov	edx, esi	 # tmp117, __r$_M_real
	imul	edx, esi	 # tmp117, __r$_M_real
	mov	edi, ebx	 # tmp116, __r$_M_imag
	imul	edi, ebx	 # tmp116, __r$_M_imag
	sub	edx, edi	 # tmp117, tmp116
	sub	eax, edx	 # tmp118, tmp117
	mov	DWORD PTR [esp+72], eax	 # d._M_real, tmp118
	mov	eax, DWORD PTR [esp+56]	 # tmp119, b._M_real
	imul	eax, ebx	 # tmp119, __r$_M_imag
	mov	edx, DWORD PTR [esp+60]	 # tmp120, b._M_imag
	imul	edx, esi	 # tmp120, __r$_M_real
	add	eax, edx	 # tmp121, tmp120
	lea	eax, [eax+ecx*4]	 # tmp126,
	mov	edx, DWORD PTR [esp+36]	 # tmp125, %sfp
	sal	edx	 # tmp125
	sub	eax, edx	 # tmp126, tmp125
	mov	DWORD PTR [esp+76], eax	 # d._M_imag, tmp126
	mov	DWORD PTR [esp+44], esi	 #, __r$_M_real
	fild	DWORD PTR [esp+44]	 #
	fstp	QWORD PTR [esp+8]	 #
	mov	DWORD PTR [esp+44], ebx	 #, __r$_M_imag
	fild	DWORD PTR [esp+44]	 #
	fstp	QWORD PTR [esp]	 #
	call	_atan2	 #
	fstp	st(0)	 #
	mov	DWORD PTR [esp+80], esi	 # D.24138._M_real, __r$_M_real
	mov	DWORD PTR [esp+84], ebx	 # D.24138._M_imag, __r$_M_imag
	lea	eax, [esp+80]	 # tmp129,
	mov	DWORD PTR [esp], eax	 #, tmp129
	call	__ZSt13__complex_absIiET_RKSt7complexIS0_E	 #
	mov	DWORD PTR [esp+44], eax	 #, D.24137
	fild	DWORD PTR [esp+44]	 #
	fstp	QWORD PTR [esp]	 #
	call	_log	 #
	mov	eax, DWORD PTR [esp+72]	 # d._M_real, d._M_real
	mov	DWORD PTR [esp+88], eax	 # D.24146._M_real, d._M_real
	mov	eax, DWORD PTR [esp+76]	 # SR.38, d._M_imag
	mov	DWORD PTR [esp+92], eax	 # D.24146._M_imag, SR.38
	lea	eax, [esp+88]	 # tmp132,
	mov	DWORD PTR [esp], eax	 #, tmp132
	fstp	QWORD PTR [esp+16]	 #
	call	__ZSt14__complex_sqrtIiESt7complexIT_ERKS2_	 #
	fnstcw	WORD PTR [esp+42]	 #
	mov	dx, WORD PTR [esp+42]	 # tmp138,
	mov	dh, 12	 # tmp138,
	mov	WORD PTR [esp+40], dx	 #, tmp138
	fld	QWORD PTR [esp+16]	 #
	fldcw	WORD PTR [esp+40]	 #
	fistp	DWORD PTR [esp+44]	 #
	fldcw	WORD PTR [esp+42]	 #
	mov	edx, DWORD PTR [esp+44]	 # tmp134,
	lea	eax, [edx-1+eax]	 # tmp133,
	lea	esp, [ebp-12]	 #,
	pop	ebx	 #
	.cfi_restore 3
	pop	esi	 #
	.cfi_restore 6
	pop	edi	 #
	.cfi_restore 7
	pop	ebp	 #
	.cfi_def_cfa 4, 4
	.cfi_restore 5
	ret
	.cfi_endproc

Czyli najgorzej nie jest, chyba nawet większy output był wygenerowany dla float-ów, aniżeli tego complex :P

0

Liczenia stałych nie liczy się.

A ten długi kod to masakra.

mov DWORD PTR [esp+92], eax	 # D.24146._M_imag, SR.38

czyli na stosie masz tu z 10 tych tmp.

80 / 8 = 10
Gorzej tego nie można obliczać choćbyś się bardzo starał.
Poprawny kod zajmowałby pewnie z kilkanaście linii.

Complex jest z reguły oparty na double; całkowite są tu bezużyteczne.

dodanie znaczników <code> i <code class="asm"> - fp

0

Zatem napisz wersję w Assemblerze, która będzie liczyła to samo, lecz będzie krótsza.
Btw, u siebie mam GCC 4.6.2, aktualnie jest już 4.8 - może tam to jakoś lepiej generuje :P

2

Moim zdaniem STL jest w tym temacie mega-przekombinowany.
Wystarczyłyby po prostu double_complex i float_complex, zoptymalizowane, i żadne template'y nie są do szczęścia potrzebne.

complex<int> to już jest taka ezoteryka że trzymanie tego w bibliotece jak by nie było standardowej jest zupełnie bez sensu.

Zresztą C99 robi to lepiej:

float complex a;
double complex b;

POZA TYM, nie widziałem jeszcze kompilatora, który potrafiłby dobrze optymalizować obliczenia na liczbach zespolonych.

Dlatego gdy prędkość obliczeń jest ważna, jakiekolwiek typy complex są bezużyteczne.

Przykładowo, naiwny, ze wzoru (pseudo)kod na fraktal Mandelbrota:

int i;
complex z,c;
...
while (abs(z) > 2.0)
{
	i++;
	z = z*z + c;
}

To będzie zbyt wolne. Oto jak trzeba liczyć:

while (z.re * z.re + z.im * z.im > 4.0)
{
	i++;
	double tmp = z.re*z.re - z.im*z.im + c.re;
	z.im = 2*z.re*z.im + c.im;
	z.re = tmp;
}

To jest to samo, ale zamiast wbudowanych funkcji lub operatorów, obliczenia są bezpośrednio na częściach rzeczywistych i urojonych. I są znacznie szybsze.

0
Azarien napisał(a):

Moim zdaniem STL jest w tym temacie mega-przekombinowany.

No, ale na obiektach zawsze tak wychodzi.
Możesz sobie zdefiniować cokolwiek, np. to o czym mówiłem na początku:
rotacje, translacje punktów, prostokątów i inne spraw z geometrii, i też dostaniesz taki piękny kodzik.

Screen |= TRect(x,y).rotate(x0,y0, a) += TPoint(dx,dy);
int i;
complex z,c;
...
while (abs(z) > 2.0)
{
	i++;
	z = z*z + c;
}

takie coś wystarczy tak przepisać:

while (abs(z) > 2.0) // to jest OK - nic tu lepszego nie stworzysz, przepisując funkcję
{
	i++;
	z.sqr() += c; // z = z*z + c;
}</cpp>

I tu będzie całkiem dobry kod... z grubsza to co tam napisałeś.

TRect(x,y).rotate(x0,y0, a) += TPoint(dx,dy);
0

tak powinno tam być:
while (norm(z) > 4.0f)

ale to i tak będzie słabe;
może lepiej byłoby liczyć na reprezentacji: z = r.exp(if), czyli polarnej; wtedy wystarczyłoby: r < 2;

a dalej: z2 = r2exp(2f),
ale z sumą byłby problem: z + a = ?

No, ale to nieważne.

Powinni poprawić te kompilatory, żeby stosowały referencję zamiast
tworzyć ciągle pomocnicze zmienne na stosie, plus kopiowanie.
Kompilator powinien brać pod uwagę co się dzieje z wynikiem - gdzie on ląduje ostatecznie.

inline operator complex + (complex a, complex b)
{ return complex(a.re + b.re, a.im+b.im); }

i teraz po wywołaniu:
z = a + b;

kompilatory realizują to tak: tmp = a + b, z = tmp;
a powinny zauważyć że tu można od razu przyjąć to z w roli tmp.

z = zsin(z) + az
tu może sobie utworzyć tmp, ale tylko jedną, a nie 4, czy więcej, a tak zwykle wychodzi - zero optymalizacji.

0

przecież to się właśnie nazywa „return value optimization”, i JEST zaimplementowane we wszystkich szeroko znanych kompilatorach C++.

Nie widzę tego typu optymalizacji w tamtym załączonym asm.

1 użytkowników online, w tym zalogowanych: 0, gości: 1