Jak dostać się do bitów zmiennej typu float?

0

Mam kod:

	float n = 123.2;
	n |= 1<<0; // error: C2296: '|=': illegal, left operand has type 'float'

Chciałbym sobie ustawić/odczytać któryś bit w zmiennej float.
Rozumiem, że operatory |= oraz &= nie są zdefiniowane dla zmiennej typu float (jeśli się mylę proszę mnie poprawić).
Jak się dostać do tego bitu?

Z int, lub char nie ma tego problemu.

0

Najbezpieczniej kopiując fragment pamięci,,, mozesz tez odpowiednio rzutować. Ale wiesz co zmieni ustawienie któregoś bitu?

0

Wiem co zmieni.
A jak skopiować fragment pamięci? np jeśli mam wskaźnik na float, jak skopiować tego floata bez żadnej konwersji do zmiennej typu int (tak, żeby był kopiowany bit w bit).
Zakładam, że float i int mają taki sam rozmiar.

Generalnie dążę do tego żeby zobaczyć jak komputer zapisał daną liczbę w pamięci.

0
static_assert(sizeof(float) == sizeof(int), "sizeof(float) != sizeof(int)");
float value = 1.f;
int value_int = 0;
memcpy(&value_int, &value, sizeof(int));
std::cout << std::hex << value_int;
1

Możesz pobrać wskaźnik float*, zrzutować na unsigned char* i oglądać bajty.

Ale uważaj, rzutowanie tak otrzymanego char* na int* żeby „zrobić sobie inta” i próba odczytania tego inta jest UB (undefined behaviour).
Podobnie niedozwolone jest rzutowanie bezpośrednio float* na int* i czytanie inta.

0
Azarien napisał(a):

Możesz pobrać wskaźnik float*, zrzutować na unsigned char* i oglądać bajty.

Ale uważaj, rzutowanie tak otrzymanego char* na int* żeby „zrobić sobie inta” i próba odczytania tego inta jest UB (undefined behaviour).
Podobnie niedozwolone jest rzutowanie bezpośrednio float* na int* i czytanie inta.

Niby co w tym ma być niedozwolone?

Format float wygląda tak:
znak * cecha * 2^mantysa
gdzie:

  1. znak liczby to najstarszy bit, czyli 31: 1 znaczy minus, a zero to +.
  2. kolejne 7 bitów, czyli 30 do 26, to cecha, z biasem 127
  3. mantysa to reszta bitów, czyli: 32 - 1 - 7 = 24 bity.

zadanie:

float x = 1.5;

i teraz robimy tak:
int &y = (int)&x;

jaką wartość ma y:
y = ?

1
float foo(float in)
{
    union {float f; unsigned u} tmp;
    
    tmp.f = in;
    tmp.u &= 0x7FFFFFFF; //przykładowe zastosowanie
    return tmp.f;
}

float bar(float in)
{
    unsigned u;

    std::memcpy(&u, &in, sizeof(float)); //kopiowanie bitów z in do u
    u &= 0x7FFFFFFF; //przykładowe zastosowanie
    std::memcpy(&in, &u, sizeof(float)); //kolejne kopiowanie z u do in

   return in;
}

Funkcja bar nie łamie strict aliasign rule, a funkcja foo oficjalnie nie jest "legalna" w c++, ale w praktyce wszystkie kompilatory ją wspierają. Poza bar, w c++ cała reszta (wskaźniki + unie) nie ma gwarancji że będzie działać na dowolnym kompilatorze zgodnym ze standardem.

0

A po co tam kasujesz znak?

union {float f; unsigned u} tmp;

tmp.f = 1.67;

return tmp.u; // to jest wersja binarna tego floata

2

zadanie:

float x = 1.5;

i teraz robimy tak:

int &y = *(int*)&x;

jaką wartość ma y:

y = ?

Tu nie chodzi o wartość, to jest UB – konkretnie ta gwiazdka tuż po znaku równości.

Kompilator może - zgodnie ze standardem - założyć, że zmiana wartości x na pewno nie zmienia wartości y. Twój kod tę zasadę (zwaną strict aliasing) łamie.

0

Wybacz, ale jakiekolwiek czytanie danych nie może łamać jakichkolwiek zasad programowania.

Ostatecznie wiadomo że pamięć w komputerze to ciągi bitów, i... więcej tam nie ma.

5

Łamie zasady (standard) języków C i C++.

Tak, ja wiem i ty wiesz, że jak zapiszemy czterobajtowego floata do n-tej komórki pamięci i odczytamy z niej czterobajtowego inta to musimy dostać takie bajty jakie nam na papierze teoretycznie wychodzą.

Ale kompilator wcale nie musi tych zmiennych zapisywać do RAM-u! Może trzymać w rejestrze procesora. Zmienne całkowite używają zupełnie innych rejestrów niż rzeczywiste.
Kiedy zapisujesz wartość do float x, kompilator zakłada, że wartość pod jakimkolwiek int* nie ulega w tym czasie zmianie. W procesie optymalizacji kodu może więc założyć, że nie trzeba tego floata zapisywać do RAM-u; albo zauważy że zapisana wartość nie jest nigdzie więcej używana, więc może zignorować zapis całkowicie.
Może się nagle okazać, że jakiś warunek zależny od tych zmiennych staje się zawsze spełniony albo nigdy nie spełniony, pętla nieskończona albo przeciwnie - martwa.

Są bardzo określone sytuacje, w których konwersja typu wskaźnika jest bezpieczna. Jedną z nich jest char i unsigned char: można float* rzutować na unsigned char* i oglądać bajty.
Można też w drugą stronę: złożyć z bajtów floata i unsigned char* rzutować na float*.

Ale nie jest to relacja przechodnia: nawet jeżeli float* -> char* nie łamie zasady strict aliasing podobnie jak char* -> int*, to nie można z tego złożyć float* -> char* -> int*. To już łamie strict aliasing.

0

Wybacz, ale to bełkot.

Poza tym wskaźnik na char to taki sam wskaźnik jak każdy inny w tym: int, double, i dowolna stryktura -
mają dokładnie taki sam adres.

Komputery są maszynami cyfrowymi - tam nie ma żadnych jakości, typu smak, ciężar, czy woń.. których się doszukują amatorzy.

0

Wybacz perlic, aleś standardu nie doczytał, bo @Azarien ma rację. Nawet jeśli nie jest to intuicyjne. ;) Fakt, to co mówisz w większości przypadków zadziała dzięki rozszerzeniom kompilatora albo po prostu z uwagi na logiczność danej operacji. Ale ma ma również 100% prawo się toto posypać i też będzie to prawidłowe.

0
alagner napisał(a):

Wybacz perlic, aleś standardu nie doczytał, bo @Azarien ma rację. Nawet jeśli nie jest to intuicyjne. ;) Fakt, to co mówisz w większości przypadków zadziała dzięki rozszerzeniom kompilatora albo po prostu z uwagi na logiczność danej operacji. Ale ma ma również 100% prawo się toto posypać i też będzie to prawidłowe.

A niby co ma się posypać w czasie odczytu bitów, które są w każdej maszynie bez wyjątku?

  1. Wiadomo jaki jest standard kodowania floatów...
  2. Problem z adresowaniem zmiennych w rejestrach nie istnieje - adres eax to eax zwyczajnie!
1

No tylko że float nie trafia do eax, a do rejestru FPU albo SSE...

Zrozum, że kompilator generuje kod z założeniem, że modyfikacja jakiegokolwiek floata nie modyfikuje żadnego inta i odwrotnie. Tak stoi w standardzie C++.

Wyjątki od strict aliasing są w standardzie określone, jest też jeden (unie) którego nie ma w standardzie ale który jest obsługiwany przez kompilatory i możesz go użyć.

0
Azarien napisał(a):

No tylko że float nie trafia do eax, a do rejestru FPU albo SSE...

Zrozum, że kompilator generuje kod z założeniem, że modyfikacja jakiegokolwiek floata nie modyfikuje żadnego inta i odwrotnie. Tak stoi w standardzie C++.

Ależ nie ma takiej możliwości - absolutnie!
Z uwagi na konieczność kodowania każdej danej na bitach.

Tym samy ten 'standard' jest nierealną utopią... jak np. sławetna komuna... której dzieje i finalny status są dobrze znane:
nie istnieje żadna komuna w tym świecie, i z prostej przyczyny - takie coś jest nierealne, bo sprzeczne z logiką!

A w praktyce wygląda to tak.

Przykładowo:
float x = 1.5;

jest tak kodowane:

fld dword ptr [5675765]

gdzie to coś: 5675765 jest adresem po którym siedzie ta liczba - w segmencie data.

versja z double - wygląda tak samo:
double x = 1.5;

fld qword ptr [5675765];
.........

wersja z sse2:
movsd xmm0, [5675765]

0
Azarien napisał(a):

Zrozum, że kompilator generuje kod z założeniem, że modyfikacja jakiegokolwiek floata nie modyfikuje żadnego inta i odwrotnie. Tak stoi w standardzie C++.

Tak z ciekawości zapytam, czy taka konstrukcja również łamie strict aliasing? Jest wskaźnik na stałą, więc o modyfikacji nie ma mowy.

float* fptr = new float( 2.5f );
const int* iptr = reinterpret_cast< const int* >( fptr );

0

Na pewno jest to nieczytelne. I pytanie czy ten reinterpret_cast cokolwiek tu zmienia. Zakładam, że nie, skoro int x = 1000; const int &r = x; jest ok.

EDIT: cholera jasna, źle przeczytałem pytanie, późna noc ;). @Azarien napisał dalej prawidłowo. Const nic tu nie zmienia.

0

@Uczynny Pomidor Zrozum, że nie chodzi tu o standardową sytuację, a o sytuację, gdy kompilator ma otwarte drzwi do optymalizowania, wtedy właśnie założenia o których jest w standardzie sa istotne.Z jednej strony wydaje się bez sensu nam na pierwszy rzut oka, ale procesor mając odpowiedni typ podczas kompilacji może dokonać pewnych optymalizacji, a Ty się zdziwisz, że nie działa hak.

0
kaczus napisał(a):

@Uczynny Pomidor Zrozum, że nie chodzi tu o standardową sytuację, a o sytuację, gdy kompilator ma otwarte drzwi do optymalizowania, wtedy właśnie założenia o których jest w standardzie sa istotne.Z jednej strony wydaje się bez sensu nam na pierwszy rzut oka, ale procesor mając odpowiedni typ podczas kompilacji może dokonać pewnych optymalizacji, a Ty się zdziwisz, że nie działa hak.

Wtedy ten kompilator byłby po prosty wadliwy, słaby, bo ograniczony jakimiś gównianym standardem.

Byłaby to sytuacja w 100% analogiczna do komuny: systemu rządów w którym rozwiązuje się z powodzeniem problemy...
ale jedynie takie urojone, bo nieistniejące w realnym świecie, jak np. sprawiedliwy podział dóbr sterowany odgórnie.

0

W związku z tematem zadanie z optymalizacji.

Kod random generatora typu double - 53 cyfry precyzji.

/* The seed-values z_hi and z_lo has to be defined as follows: /
/
printf("Input seed-high (0 ... 2^27 - 1) --> "); /
/
scanf("%ld", &z_hi); /
/
printf("Input seed-low (0 ... 2^25 - 1) --> "); /
/
scanf("%ld", &z_lo); z_lo = z_lo4L + 1L; /
/
-------------------------------------------------------------------------
/
uint z_lo = 65530197, z_hi = 3698544;
void drand_i()
{
z_lo = _lrand() >> 5;
}

double drand()
{
union ux {
double d;
int16 i[4];
} x;

uint32  i_x;
double d_x;

x.d = (double)(z_lo) * 59760077.0;  x.i[3] -= 0X01b0;
i_x = x.d;
    z_hi = (z_hi * 59760077L + z_lo * 20737779L + i_x) & 0X07FFFFFFL;
d_x = x.d -= (double)i_x;            x.i[3] += 0X01b0;
    z_lo = x.d;
x.d = (double)z_hi + d_x;           x.i[3] -= 0X01b0;

return (x.d);

}

proszę mi to teraz przerobić - zgodnie z tym standardem o niemożliwości modyfikowania double... itd.
Potem porównamy wydajność tego kodu z tym tu.

2
tajny_agent napisał(a):

Tak z ciekawości zapytam, czy taka konstrukcja również łamie strict aliasing? Jest wskaźnik na stałą, więc o modyfikacji nie ma mowy.

float* fptr = new float( 2.5f );
const int* iptr = reinterpret_cast< const int* >( fptr );

Nie łamie, bo nie dereferujesz iptr. Ale jakiekolwiek użycie *iptr zasadę złamie. Nie musi być modyfikacji.

4
Uczynny Pomidor napisał(a):

Wtedy ten kompilator byłby po prosty wadliwy, słaby, bo ograniczony jakimiś gównianym standardem.

Standard jest jaki jest, a strict aliasing nie ogranicza kompilatora tylko daje możliwość lepszej optymalizacji kodu.
Ogranicza to co najwyżej programistę.

Wyobraź sobie kod:

void print_int(int);
void print_float(float);

int main()
{
    int a = 42;
    print_int(a);
    float b = 3.14;
    print_float(pi);
}

Kompilator oczywiście mógłby zapisać inta do jednej komórki RAM-u, odczytać go, przekazać do print_int, zapisać floata do drugiej komórki RAM, odczytać go i przekazać do print_float.
Ale nie musi. Może ten kod zamienić na coś takiego:

    print_int(42);
    print_float(3.14);

i jeżeli na danej architekturze parametry można przesyłać w rejestrach a nie np. na stosie, to te wartości mogą w ogóle nie mieć reprezentacji w RAM-ie. Jeżeli procesor ma osobne rejestry na liczby całkowite i zmiennoprzecinkowe, to będą to zupełnie różne rejestry.

Teraz wyobraź sobie, taki kod:

    int a = 42;
    int *pa = &a;
    float *pf = (float*)pa;
    *pf = 3.14;
    print_int(a);

Co my tu mamy?

  1. int a równe 42. Na razie ten int może być w rejestrze albo być tylko umownym intem.
  2. Oho, pobierany jest wskaźnik na a, czyli a jednak musi być w pamięci, bo potrzebny jest adres.
  3. Reinterpret cast. Sam w sobie dozwolony.
  4. Zapisanie wartości float pod wskaźnikiem pf. Nie ma problemu.
  5. Wypisanie a. Ile wynosi a? Czy zmieniło się od pierwszego punktu? NIE! nigdzie nie ma instrukcji, która by zapisywała pod pa ani pod jakikolwiek inny int* z ewentualną indeksacją. Zatem nie musimy czytać wartości a z RAM-u. Na pewno wynosi nadal 42, więc możemy zrobić tutaj print_int(42) z wartością immediate albo z rejestru, dzięki czemu mamy szybszy kod.

ZONK.

Tak działają współczesne kompilatory. Tak jest zgodnie ze standardem języka C++: zapis do float* na pewno nie modyfikuje inta, i odwrotnie.
Kompilator nie może śledzić wszystkich wskaźników czy na pewno nie strzelasz sobie w stopę. Może w tak prostym przypadku mógłby, ale w przypadku ogólnym jest to niewykonalne (tzw. problem stopu).

Powyższy kod według standardu ma undefined behavior z powodu linii 4, gdzie następuje dereferencja wskaźnika float* mimo że w RAMie pod tym adresem znajduje się int.

Napisałem „z powodu linii 4” a nie „w linii 4”, bo UB dotyczy zachowania całego programu, a nie tylko w danej linii albo tylko od danej linii.

0

Najwyraźniej wpadłeś utopię.

Fakt:
nie ma znaczenia co i jak będziesz kombinował, finalnie i tak musisz zapisać w pamięci - każdą daną jaką sobie wymyślisz.

Ty kombinujesz tak:
float x = 1.5;

i chcesz to wsadzić od razu do jakiegoś rejestru - bez zapisu w segmencie danych - zgadza się?

No to wtedy musisz (znaczy kompilator musi) to zapisać w kodzie!

np. tak:

mov eax, 7686667;
gdzie to coś: 7686667 jest właśnie 1.5, ale w wersji int32

A potem musiałby to załadować do FPU, zatem taki kod:

mov [ram], eax;
fld ram

tak byłoby w wersji z FPU - mało sensowne.

Natomiast z SSE2 mogłoby to być sensowne, bo wyglądałoby jakoś tak:

movss xmm0, 7686667
o ile w ogóle istniej rozkaz ładowania tego typu w sse2... ładowania wart. natychmiastowej.

potem można od razu na tym wykonywać obliczenia, np.:
addss xmm0, xmm2; // single suma floatów

........
Reasumują: każda liczba bez wyjątku musi być i jest gdzieś zapisywana - gdzie? w ram, oczywiście!
Jak nie w segmencie danych, no to w wprost w kodzie - w parametrach instrukcji, jako wartość natychmiastowa rozkazu.

6

Już mi się nie chce.

0
Azarien napisał(a):

Już mi się nie chce.

Po prostu masz problem z adresowaniem danych -
podobnie ja autorzy tego idiotycznego pseudstandardu.

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