Czy kompilator może zoptymalizować lokalną zmienną lotną?

79

Czy kompilator może to zoptymalizować (zgodnie ze standardem C ++ 17):

int fn() {
    volatile int x = 0;
    return x;
}

do tego?

int fn() {
    return 0;
}

Jeśli tak, dlaczego? Jeśli nie, dlaczego nie?


Oto kilka przemyśleń na ten temat: obecne kompilatory kompilują się fn()jako zmienna lokalna umieszczana na stosie, a następnie zwracają ją. Na przykład na x86-64, gcc tworzy to:

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret    

O ile wiem, standard nie mówi, że lokalna zmienna zmienna powinna być umieszczona na stosie. Tak więc ta wersja byłaby równie dobra:

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret    

Tutaj edxsklepy x. Ale teraz, po co się tu zatrzymać? Jak edxi eaxto zarówno zero, możemy tylko powiedzieć:

xor    eax,eax // eax is the return, and x as well
ret    

I przeszliśmy fn()do wersji zoptymalizowanej. Czy ta transformacja jest ważna? Jeśli nie, który krok jest nieprawidłowy?

geza
źródło
1
Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
@philipxy: Nie chodzi o to, „co można wyprodukować”. Chodzi o to, czy transformacja jest dozwolona. Ponieważ jeśli nie jest to dozwolone, nie może tworzyć przekształconej wersji.
geza
Standard definiuje dla programu sekwencję dostępów do składników lotnych i innych obserwowalnych, które implementacja musi przestrzegać. Ale dostęp do niestabilnych środków zależy od implementacji. Nie ma więc sensu pytać, co może wytworzyć implementacja - produkuje to, co ma produkować. Mając pewien opis zachowania podczas implementacji, możesz poszukać innego, który wolisz. Ale potrzebujesz go, aby zacząć. Być może faktycznie jesteś zainteresowany obserwowalnymi regułami standardu, ponieważ generowanie kodu jest nieistotne poza koniecznością spełnienia reguł standardu i implementacji.
philipxy
1
@philipxy: Wyjaśnię moje pytanie, że chodzi o standard. Zwykle wynika to z tego rodzaju pytań. Interesuje mnie, co mówi norma.
geza

Odpowiedzi:

63

Nie. Dostęp do volatileobiektów jest uważany za obserwowalne zachowanie, dokładnie tak, jak we / wy, bez szczególnego rozróżnienia między lokalnymi i globalnymi.

Najmniejsze wymagania dotyczące zgodnej implementacji to:

  • Dostęp do volatileobiektów jest oceniany ściśle według reguł abstrakcyjnej maszyny.

[…]

Są one łącznie określane jako obserwowalne zachowanie programu.

N3690, [intro.execution], ¶8

To , w jaki sposób można to dokładnie zaobserwować, wykracza poza zakres standardu i dotyczy bezpośrednio obszaru specyficznego dla implementacji, dokładnie tak jak we / wy i dostęp do volatileobiektów globalnych . volatileoznacza "myślisz, że wiesz wszystko, co się tutaj dzieje, ale tak nie jest; zaufaj mi i rób to bez bycia zbyt sprytnym, ponieważ jestem w twoim programie i robię swoje tajne rzeczy z twoimi bajtami". Faktycznie jest to wyjaśnione w [dcl.type.cv] ¶7:

[Uwaga: volatilejest wskazówką do implementacji, aby uniknąć agresywnej optymalizacji związanej z obiektem, ponieważ wartość obiektu może zostać zmieniona w sposób niewykrywalny przez implementację. Ponadto w przypadku niektórych implementacji zmienna nietrwała może wskazywać, że dostęp do obiektu wymaga specjalnych instrukcji sprzętowych. Zobacz 1.9 dla szczegółowej semantyki. Ogólnie rzecz biorąc, semantyka zmiennej volatile ma być taka sama w C ++, jak w C. - uwaga końcowa]

Matteo Italia
źródło
2
Ponieważ jest to pytanie cieszące się największym uznaniem, a pytanie zostało rozszerzone w wyniku edycji, byłoby miło poddać edycji tę odpowiedź w celu omówienia nowych przykładów optymalizacji.
hyde
Prawidłowe to „tak”. Ta odpowiedź nie pozwala wyraźnie odróżnić abstrakcyjnych obserwacji maszynowych od wygenerowanego kodu. Ta ostatnia jest zdefiniowana w ramach implementacji. Np. Być może do użycia z danym debugerem, obiekt ulotny na pewno znajduje się w pamięci i / lub rejestrze; np. zazwyczaj w ramach odpowiedniej architektury docelowej gwarantowane są zapisy i / lub odczyty dla obiektów ulotnych w specjalnych lokalizacjach pamięci określonych w pragmie. Implementacja definiuje, w jaki sposób dostępy są odzwierciedlane w kodzie; decyduje o tym, jak i kiedy obiekt (y) „mogą zostać zmienione w sposób niewykrywalny przez implementację”. (Zobacz moje komentarze do pytania.)
philipxy
12

Ta pętla może zostać zoptymalizowana za pomocą reguły as-if, ponieważ nie ma obserwowalnego zachowania:

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

Ten nie może:

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

Druga pętla robi coś w każdej iteracji, co oznacza, że ​​pętla zajmuje O (n) czasu. Nie mam pojęcia, jaka jest stała, ale mogę ją zmierzyć i wtedy mam sposób na zapętlenie przez (mniej lub bardziej) znany czas.

Mogę to zrobić, ponieważ standard mówi, że dostęp do substancji lotnych musi mieć miejsce, w porządku. Gdyby kompilator zdecydował, że w tym przypadku standard nie ma zastosowania, myślę, że miałbym prawo zgłosić błąd.

Jeśli kompilator zdecyduje się umieścić loopedw rejestrze, przypuszczam, że nie mam przeciwko temu dobrego argumentu. Ale nadal musi ustawić wartość tego rejestru na 1 dla każdej iteracji pętli.

rici
źródło
Więc, czy mówisz, że ostateczna wersja xor ax, ax(gdzie axjest uważana za znajdującą się volatile x) w pytaniu jest ważna, czy nieważna? IOW, jaka jest twoja odpowiedź na pytanie?
hyde
@hyde: Pytanie, jak to czytałem, brzmiało: „czy można usunąć zmienną”, a moja odpowiedź brzmi „Nie”. Jeśli chodzi o konkretną implementację x86, która rodzi pytanie, czy ulotność można umieścić w rejestrze, nie jestem do końca pewien. Nawet jeśli jest zredukowany do xor ax, axtego, tego kodu nie można wyeliminować, nawet jeśli wygląda na bezużyteczny, ani nie można go scalić. W moim przykładzie pętli skompilowany kod musiałby zostać wykonany xor ax, axn razy, aby spełnić regułę obserwowalnego zachowania. Mam nadzieję, że zmiana odpowiada na Twoje pytanie.
rici
Tak, pytanie zostało nieco rozszerzone przez edycję, ale ponieważ odpowiedziałeś po edycji, pomyślałem, że ta odpowiedź powinna obejmować nową część ...
hyde
2
@hyde: W rzeczywistości używam w ten sposób składników lotnych w testach porównawczych, aby uniknąć optymalizacji przez kompilator pętli, która w przeciwnym razie nic nie robi. Więc naprawdę mam nadzieję, że mam rację: =)
rici
Norma mówi, że operacje na volatileobiektach są - same w sobie - rodzajem efektu ubocznego. Implementacja mogłaby zdefiniować ich semantykę w sposób, który nie wymagałby od nich generowania żadnych rzeczywistych instrukcji procesora, ale pętla, która uzyskuje dostęp do obiektu o zmiennej zmienności, ma efekty uboczne, a zatem nie kwalifikuje się do usunięcia.
supercat
10

Błagam o odmowę zdania większości, pomimo pełnego zrozumienia, które volatileoznacza obserwowalne I / O.

Jeśli masz ten kod:

{
    volatile int x;
    x = 0;
}

Uważam, że kompilator może go zoptymalizować zgodnie z regułą as-if , zakładając, że:

  1. W volatileinnym przypadku zmienna nie jest widoczna na zewnątrz za pomocą np. Wskaźników (co oczywiście nie stanowi tutaj problemu, ponieważ nie ma czegoś takiego w danym zakresie)

  2. Kompilator nie zapewnia mechanizmu zewnętrznego dostępu do tego volatile

Uzasadnieniem jest po prostu to, że i tak nie można było zaobserwować różnicy ze względu na kryterium nr 2.

Jednak w twoim kompilatorze kryterium nr 2 może nie być spełnione ! Kompilator może próbować zapewnić dodatkowe gwarancje dotyczące obserwacji volatilezmiennych z „zewnątrz”, na przykład poprzez analizę stosu. W takich sytuacjach zachowanie jest naprawdę obserwowalne, więc nie można go zoptymalizować.

Teraz pytanie brzmi, czy poniższy kod różni się od powyższego?

{
    volatile int x = 0;
}

Wydaje mi się, że zaobserwowałem inne zachowanie tego w Visual C ++ w odniesieniu do optymalizacji, ale nie jestem do końca pewien z jakich powodów. Być może inicjalizacja nie liczy się jako „dostęp”? Nie jestem pewny. To może być warte osobnego pytania, jeśli jesteś zainteresowany, ale poza tym uważam, że odpowiedź jest taka, jak wyjaśniłem powyżej.

user541686
źródło
6

Teoretycznie mógłby to zrobić program obsługi przerwań

  • sprawdź, czy adres zwrotny mieści się w fn() funkcji. Może uzyskać dostęp do tablicy symboli lub numerów linii źródłowych za pośrednictwem oprzyrządowania lub dołączonych informacji debugowania.
  • następnie zmień wartość x, która byłaby przechowywana z przewidywalnym przesunięciem względem wskaźnika stosu.

… W ten sposób fn()zwracając wartość różną od zera.

podążył za Monicą do Codidact
źródło
1
Możesz też łatwiej to zrobić za pomocą debugera, ustawiając punkt przerwania w programie fn(). Użycie volatiletworzy kod-gen, który jest podobny gcc -O0do tej zmiennej: spill / reload pomiędzy każdą instrukcją C. ( -O0nadal można łączyć wiele dostępów w jednej instrukcji bez naruszania spójności debugera, ale volatilenie jest to dozwolone).
Peter Cordes
Albo prościej, używając debuggera :) Ale który standard mówi, że zmienna musi być obserwowalna? To znaczy, implementacja może wybrać, że musi być obserwowalna. Ktoś inny może powiedzieć, że nie da się tego zaobserwować. Czy ten ostatni narusza standard? Może nie. Norma nie określa, w jaki sposób lokalna zmienna zmienna może w ogóle być obserwowalna.
geza
A nawet, co to znaczy „obserwowalne”? Czy powinien być umieszczony na stosie? A jeśli rejestr jest przechowywany x? A co, jeśli na x86-64 xor rax, raxprzechowuje zero (mam na myśli, że rejestr wartości zwracanej przechowuje x), co oczywiście może być łatwo obserwowane / modyfikowane przez debugger (tj. Informacje o symbolach debugowania xsą przechowywane w rax). Czy to narusza standard?
geza
2
−1 Każde wywołanie do fn()może być wstawione. Z MSVC 2017 i domyślnym trybem wydania jest. Nie ma więc „w ramach fn()funkcji”. Niezależnie od tego, ponieważ zmienna jest zapisywana automatycznie, nie ma „przewidywalnego przesunięcia”.
Pozdrawiam i hth. - Alf
1
0 @berendi: Tak, masz rację, a ja się myliłem. Przepraszam, zły poranek pod tym względem (źle dwa razy). Mimo to IMO nie ma sensu spierać się, w jaki sposób kompilator może obsługiwać dostęp za pośrednictwem innego oprogramowania, ponieważ może to zrobić niezależnie od tego volatile, i ponieważ volatilenie zmusza go do zapewnienia takiej obsługi. Dlatego usuwam głos przeciw (myliłem się), ale nie głosuję za, ponieważ myślę, że ten tok rozumowania nie wyjaśnia.
Pozdrawiam i hth. - Alf,
6

Dodam tylko szczegółowe odniesienie do reguły as-if i zmiennej niestabilnej słowa kluczowego . (Na dole tych stron postępuj zgodnie z instrukcjami „zobacz także” i „Referencje”, aby prześledzić oryginalne specyfikacje, ale wydaje mi się, że cppreference.com jest znacznie łatwiejszy do odczytania / zrozumienia).

W szczególności chcę, abyś przeczytał tę sekcję

obiekt ulotny - obiekt, którego typ jest nietrwały, lub podobiekt obiektu ulotnego lub zmienny podobiekt obiektu o stałej zmienności. Każdy dostęp (operacja odczytu lub zapisu, wywołanie funkcji składowej itp.) Dokonany za pomocą wyrażenia glvalue typu volatile-qualified jest traktowany jako widoczny efekt uboczny na potrzeby optymalizacji (to znaczy w ramach pojedynczego wątku wykonania, volatile Dostępów nie można zoptymalizować lub zmienić ich kolejności z innym widocznym efektem ubocznym, który jest sekwencjonowany przed lub sekwencjonowany po dostępie ulotnym. To sprawia, że ​​obiekty ulotne nadają się do komunikacji z programem obsługi sygnału, ale nie z innym wątkiem wykonania, zobacz std :: memory_order ). Każda próba odniesienia się do obiektu ulotnego za pomocą nieulotnej wartości typu glvalue (np. Poprzez odniesienie lub wskaźnik do typu nieulotnego) skutkuje niezdefiniowanym zachowaniem.

Więc zmienne słowo kluczowe szczególności wyłączania optymalizacji kompilatora na glvalues . Jedyną rzeczą, na którą słowo kluczowe volatile może mieć wpływ, jest prawdopodobnie to return x, że kompilator może zrobić, co zechce, z resztą funkcji.

To, jak bardzo kompilator może zoptymalizować zwrot, zależy od tego, jak bardzo kompilator może zoptymalizować dostęp x w tym przypadku (ponieważ nie zmienia kolejności, a ściśle mówiąc, nie usuwa wyrażenia zwrotnego. Jest dostęp , ale to odczytuje i zapisuje na stosie, co powinno być w stanie usprawnić.) Więc kiedy to czytam, jest to szary obszar określający, ile kompilator może optymalizować, i można go łatwo spierać w obie strony.

Uwaga dodatkowa: w takich przypadkach zawsze zakładaj, że kompilator zrobi odwrotnie niż chciałeś / potrzebowałeś. Powinieneś albo wyłączyć optymalizację (przynajmniej dla tego modułu), albo spróbować znaleźć bardziej zdefiniowane zachowanie dla tego, co chcesz. (To jest również powód, dla którego testowanie jednostkowe jest tak ważne) Jeśli uważasz, że to wada, powinieneś porozmawiać o tym z twórcami C ++.


To wszystko jest nadal bardzo trudne do odczytania, więc spróbuj uwzględnić to, co uważam za istotne, abyś mógł przeczytać to sam.

glvalue Wyrażenie glvalue to lwartość lub xwartość.

Nieruchomości:

Glvalue może być niejawnie konwertowane na prvalue z niejawną konwersją l-wartość-na-r-wartość, tablica-wskaźnik lub funkcja-wskaźnik. Wartość gl może być polimorficzna: dynamiczny typ obiektu, który identyfikuje, niekoniecznie jest statycznym typem wyrażenia. Wartość glvalue może mieć niekompletny typ, jeśli zezwala na to wyrażenie.


xvalue Następujące wyrażenia są wyrażeniami xvalue:

wywołanie funkcji lub przeciążone wyrażenie operatora, którego zwracanym typem jest odwołanie do obiektu rvalue, na przykład std :: move (x); a [n], wbudowane wyrażenie w indeksie dolnym, gdzie jeden operand jest tablicą rwartość; am, element członkowski wyrażenia obiektowego, gdzie a jest wartością r, a m jest niestatycznym składnikiem danych typu niereferencyjnego; a. * mp, wskaźnik do elementu członkowskiego wyrażenia obiektu, gdzie a jest wartością r, a mp jest wskaźnikiem do elementu danych; a? b: c, trójskładnikowe wyrażenie warunkowe dla niektórych b i c (szczegóły w definicji); wyrażenie rzutowania na rvalue odniesienie do typu obiektu, takie jak static_cast (x); każde wyrażenie, które oznacza tymczasowy obiekt po tymczasowej materializacji. (od C ++ 17) Właściwości:

To samo co rvalue (poniżej). To samo co wartość kleju (poniżej). W szczególności, podobnie jak wszystkie wartości r, wartości x wiążą się z odwołaniami do wartości r i podobnie jak wszystkie wartości gl, wartości x mogą być polimorficzne, a wartości x niebędące klasami mogą być kwalifikowane jako cv.


lwartość Następujące wyrażenia są wyrażeniami lwartości:

nazwa zmiennej, funkcji lub elementu członkowskiego danych, niezależnie od typu, na przykład std :: cin lub std :: endl. Nawet jeśli typ zmiennej to odwołanie do wartości r, wyrażenie składające się z jej nazwy jest wyrażeniem l-wartości; wywołanie funkcji lub wyrażenie przeciążonego operatora, którego zwracanym typem jest odwołanie do lvalue, na przykład std :: getline (std :: cin, str), std :: cout << 1, str1 = str2 lub ++ it; a = b, a + = b, a% = b oraz wszystkie inne wbudowane i złożone wyrażenia przypisania; ++ a i --a, wbudowane wyrażenia preinkrementacji i dekrementacji; * p, wbudowane wyrażenie pośrednie; a [n] i p [n], wbudowane wyrażenia z indeksem dolnym, z wyjątkiem sytuacji, gdy a jest tablicą rvalue (od C ++ 11); am, element członkowski wyrażenia obiektu, z wyjątkiem sytuacji, gdy m jest elementem wyliczającym składowym lub niestatyczną funkcją składową, lub gdzie a jest wartością r, a m jest niestatycznym składnikiem danych typu niereferencyjnego; p-> m, wbudowany element członkowski wyrażenia wskaźnika, z wyjątkiem sytuacji, gdy m jest elementem wyliczającym składowym lub niestatyczną funkcją składową; a. * mp, wskaźnik do elementu członkowskiego wyrażenia obiektu, gdzie a jest lwartością, a mp jest wskaźnikiem do elementu danych; p -> * mp, wbudowany wskaźnik do elementu członkowskiego wyrażenia wskaźnika, gdzie mp jest wskaźnikiem do elementu członkowskiego danych; a, b, wbudowane wyrażenie z przecinkiem, gdzie b jest lwartością; a? b: c, trójskładnikowe wyrażenie warunkowe dla niektórych b i c (np. gdy oba są wartościami tego samego typu, ale szczegóły w definicji); literał ciągu, taki jak „Hello, world!”; wyrażenie rzutowania na typ referencyjny lvalue, taki jak static_cast (x); wywołanie funkcji lub przeciążone wyrażenie operatora, którego zwracanym typem jest rvalue odwołanie do funkcji; wyrażenie rzutowania na rvalue odniesienie do typu funkcji, takie jak static_cast (x). (od C ++ 11) Właściwości:

To samo co wartość kleju (poniżej). Można przyjąć adres lwartości: & ++ i 1 i & std :: endl są poprawnymi wyrażeniami. Modyfikowalna lwartość może być użyta jako lewostronny operand wbudowanego przypisania i złożonych operatorów przypisania. Do zainicjowania odwołania do lwartości można użyć lwartości; to wiąże nową nazwę z obiektem zidentyfikowanym przez wyrażenie.


zasada jak gdyby

Kompilator C ++ może wprowadzać zmiany w programie, o ile spełnione są następujące warunki:

1) W każdym punkcie sekwencji wartości wszystkich obiektów ulotnych są stabilne (poprzednie oceny są zakończone, nowe oceny nie zostały rozpoczęte) (do C ++ 11) 1) Dostęp (odczyt i zapis) do obiektów ulotnych zachodzi ściśle zgodnie z semantyką wyrażeń, w których występują. W szczególności nie są one zmieniane w kolejności w odniesieniu do innych nietrwałych dostępów w tym samym wątku. (od C ++ 11) 2) W momencie zakończenia programu dane zapisywane do plików są dokładnie takie, jakby program był wykonywany tak, jak został zapisany. 3) Tekst podpowiedzi, który jest wysyłany do urządzeń interaktywnych, zostanie wyświetlony, zanim program zaczeka na wprowadzenie. 4) Jeśli ISO C pragma #pragma STDC FENV_ACCESS jest obsługiwana i ustawiona na ON,


Jeśli chcesz przeczytać specyfikacje, uważam, że to są te, które musisz przeczytać

Bibliografia

Norma C11 (ISO / IEC 9899: 2011): 6.7.3 Kwalifikatory typu (p: 121-123)

Norma C99 (ISO / IEC 9899: 1999): 6.7.3 Kwalifikatory typu (p: 108-110)

Norma C89 / C90 (ISO / IEC 9899: 1990): 3.5.3 Kwalifikatory typu

Tezra
źródło
Może to nie być poprawne zgodnie ze standardem, ale każdy, kto polega na tym, że stos zostanie dotknięty przez coś innego podczas wykonywania, powinien przestać kodować. Twierdzę, że to standardowa wada.
meneldal
1
@meneldal: To zbyt szerokie twierdzenie. Użycie _AddressOfReturnAddressobejmuje na przykład analizę stosu. Ludzie analizują stos z ważnych powodów i niekoniecznie dlatego, że sama funkcja polega na nim pod względem poprawności.
user541686
1
glvalue jest tutaj:return x;
geza
@geza Przepraszamy, to wszystko jest trudne do odczytania. Czy to jest glvalue, ponieważ x jest zmienną? Co więcej, dla „nie można zoptymalizować na zewnątrz”, czy oznacza to, że kompilator nie może w ogóle zoptymalizować, czy też nie może zoptymalizować przez zmianę wyrażenia? (Wygląda na to, że kompilator nadal może tutaj optymalizować, ponieważ nie ma kolejności dostępu do utrzymania, a wyrażenie jest nadal rozwiązywane, tylko w bardziej zoptymalizowany sposób). okular.
Tezra,
Oto cytat z Twojej własnej odpowiedzi :) "Następujące wyrażenia są wyrażeniami lwartości: nazwa zmiennej ..."
geza
-1

Myślę, że nigdy nie widziałem zmiennej lokalnej używającej volatile, która nie byłaby wskaźnikiem do zmiennej volatile. Jak w:

int fn() {
    volatile int *x = (volatile int *)0xDEADBEEF;
    *x = 23;   // request data, 23 = temperature 
    return *x; // return temperature
}

Jedyne inne przypadki niestabilności, jakie znam, używają globalnego, który jest zapisany w obsłudze sygnału. Nie ma tam żadnych wskazówek. Lub dostęp do symboli zdefiniowanych w skrypcie konsolidatora pod określonymi adresami dotyczącymi sprzętu.

O wiele łatwiej jest tam uzasadnić, dlaczego optymalizacja miałaby zmienić obserwowalne efekty. Ale ta sama reguła dotyczy lokalnej zmiennej lotnej. Kompilator musi zachowywać się tak, jakby dostęp do x był obserwowalny i nie może go zoptymalizować.

Goswin von Brederlow
źródło
3
Ale to nie jest lokalna zmienna ulotna, jest to lokalny nieulotny wskaźnik do ulotnego int pod dobrze znanym adresem.
Bezużyteczne
Co ułatwia wnioskowanie o właściwym zachowaniu. Jak powiedziano, zasady dostępu do zmiennych nietrwałych są takie same dla zmiennych lokalnych i wskaźników do zmiennych nietrwałych, które są wyłuskiwane.
Goswin von Brederlow
Odnoszę się tylko do pierwszego zdania Twojej odpowiedzi, które wydaje się sugerować, że xw Twoim kodzie jest „lokalna zmienna zmienna”. Tak nie jest.
Bezużyteczne
Wściekłem się, gdy int fn (const volatile int argument) nie zostało skompilowane.
Joshua,
4
Zmiana sprawia, że ​​twoja odpowiedź nie jest błędna, ale po prostu nie odpowiada na pytanie. To jest podręcznik użycia dla volatilei nie ma nic wspólnego z tym, że jest lokalny. Równie dobrze mógłby mieć static volatile int *const x = ...zasięg globalny i wszystko, co powiesz, byłoby dokładnie takie samo. To jest jak dodatkowa wiedza, która jest niezbędna do zrozumienia pytania, na które chyba nie każdy ma, ale nie jest to prawdziwa odpowiedź.
Peter Cordes,