Пользователь
0,0
рейтинг
20 февраля 2011 в 07:42

Разработка → Про C++ алиасинг, ловкие оптимизации и подлые баги

C++*
С удивлением обнаружил, что про явление алиасинга (aliasing) здесь постов нет. Ситуацию нужно исправить, тк. алиасинг в любой сколько-то сложной C++ программе обязательно хоть где-нибудь, да есть. Это может быть хорошо, давая возможность ловких оптимизаций, а может быть плохо, внося повышенной паршивости баги. Под катом вкратце про оба случая (ну и неизменное «компилятор бьет спина», конечно; для разнообразия сегодня это gcc).

Про aliasing



Что такое aliasing? Очень просто. Это когда на один и тот же участок памяти показывают несколько разных указателей. Например.

int A;
int * B = &A;
int * C = &A;


В этом примере у переменной A внезапно три разных имени (alias): A, *B, *C. Это совершенно легальный код. Компилятор успешно обработает все 3 имени, если в A что-нибудь запишут, то через *B это будет можно прочитать и наоборот, все хорошо.

Про оптимизации и __restrict



За исключением одной мелочи: возможных оптимизаций. Компилятор обязан понимать и помнить про aliasing не только в таком наглядном случае, но и там, где человек неявно никаких алиасов не предполагает. Например.

void SumIt ( int * out, const int * in, int count )
{
      for ( int i=0; i<count; i++ )
              (*out) += *in++;
}


Обычная функция с out-параметром, ничто не предвещает беды. А она есть. Компилятору никто не дал гарантий, что out-переменная по адресу *out решительно не пересекается с in-данными по адресу *in. Самостоятельно он таких предположений сделать не имеет права: мало ли, как и почему человек захочет написать? Поэтому на каждой итерации внутреннего цикла происходит запись *out обратно в память, даже при максимальном уровне оптимизации. Дизасм (gcc -O3 -S, 4.4.3, ubuntu x64) выглядит вот так.

.L7:
      addq    $4, %rax
      addl    (%rsi), %ecx
      cmpq    %rdx, %rax
      movq    %rax, %rsi
      movl    %ecx, (%rdi) ; <-- вот оно!
      jne     .L7


Компилятору однако можно подсказать, что out никак не пересекается с in. Для этого человечество придумало модификатор __restrict.

void SumIt ( int * __restrict out, const int * __restrict in, int count )
{
      for ( int i=0; i<count; i++ )
              (*out) += *in++;
}


.L14:
      addq    $4, %rax
      addl    (%rsi), %ecx
      cmpq    %rdx, %rax
      movq    %rax, %rsi
      jne     .L14
      movl    %ecx, (%rdi)


Ну, подумаешь, 1 инструкция? Процессоры нынче умные, с толстым кешом и кучей конвейеров. В этом мини-примере запись, конечно, мгновенно закешируется, лишняя инструкция небось спараллелится с чем-нибудь, и отличия небось даже и измерить не удастся?

1783293664 in 103818 usec
1783293664 in 69818 usec


Не совсем. Удается. Упс, ускорение примерно эдак в 1.5 раза. Такая вот местами бывает цена одной инструкции (и двух модификаторов). Обычно все равно, но для хорошо нагруженных внутренних циклов полезно.

Про strict aliasing и баги



Как видим, устранение алиасинга может вылиться в неплохое улучшение скорости. Видимо, из этих соображений в стандарте C99, а через это и C++, придумали и ввели правило про strict aliasing. Ссылка для людей, владеющих мастерством чтения и понимания Стандарта: N1124, 6.5(7). Нормальному человеку туда смотреть не очень стоит: например, ни слова strict, ни слова aliasing в этом абзаце нет. ;) (Найти его сколько-то быстро удалось только потому, что в сноске номер 74 есть слово aliased.) Особо важный прикладной смысл «на пальцах» однако можно пояснить довольно просто.

В режиме strict aliasing компилятор считает, что объекты, на которые показывают указатели «существенно различных» типов, НЕ могут храниться в одном и том же участке памяти, и может использовать это при оптимизациях.

Что совершенно не важно, когда указатели либо действительно показывают в разные места, либо используются достаточно далеко друг от друга. Но смертельно, когда указатели показывают в одну и ту же память, используются рядом, а компилятор это gcc.

#include <stdio.h>

typedef unsigned int DWORD;
typedef unsigned short WORD;

inline DWORD SwapWords ( DWORD val )
{
      WORD * p = (WORD*) &val;
      WORD t;
      t = p[0];
      p[0] = p[1];
      p[1] = t;
      return val;
}

int main()
{
      printf ( "%d\n", SwapWords(1) );
      return 0;
}


Эта незатейливая программка печатает либо 65536 при сборке g++ test.cpp, либо 1 при сборке g++ -O3 test.cpp. Что за дела?!

Дело в том, что начиная с -O2 автоматом включается -fstrict-aliasing. И компилятор считает, что *p в принципе не может показывать туда, где хранится val. И успешно это дело оптимизирует насмерть: раз не может, значит нам вернут значение аргумента; значит SwapWords(1) можно заменить просто на константу 1.

Причем в данном примере проблемы, на самом деле, не очень есть. Ибо если включить -Wall (ну или хотя бы -Wstrict-aliasing), компилятор честно пожалуется на непонятное.

test.cpp:8: warning: dereferencing pointer 'p.14' does break strict-aliasing rules


Что нетяжело исправить. Детсадовский метод, это отключить проклятый strict aliasing совсем ключиком -fno-strict-aliasing. Уставной способ исправления, который де-факто работает везде и всюду, это протянуть значение через union. Полям union вроде как любой компилятор милостиво дозволяет алиасится друг с другом. Любые фокусы с указателями теоретически могут вылиться боком (undefined behavior), в случае gcc теория нетяжело превращается в практику и обратно (-fstrict-aliasing).

inline DWORD SwapWords ( DWORD val )
{
	union { DWORD d; WORD v[2]; } u;
	u.d = val;
	WORD t = u.v[0];
	u.v[0] = u.v[1];
	u.v[1] = t;
	return u.d;
}


Ура-ура? Но увы, есть одно маленькое но: -Wstrict-aliasing ничего не гарантирует. Для поимки всех случаев алиасинга, не совместимых с текущим режимом компиляции, его недостаточно. Достаточно короткого и потому наглядного примера у меня нет (есть слишком длинный), поэтому придется поверить на слово: совсем немного шаблонного фарша, функтор-другой, и strict aliasing ловко маскируется и ворнинга не дает. В программе с активным использованием STL и-или Boost, подозреваю, незаметно нарушить strict aliasing где-нибудь в дебрях кода должно быть довольно нетяжело. Третьи лица также свидетельствуют, что фокусы с приведением к void* и обратно успешно подавляли warning как минимум на gcc 4.1.x, при этом оставляя генерацию кривого кода, разумеется.

Несмотря на undefined behavior винт оно, конечно, не отформатирует. (Ну, не сразу.) Однако переставить местами чтение и запись в память в целях оптимизации компилятор может запросто. Выглядит это примерно вот так.

inline uint64_t GetIt ( const DWORD * p )
{
      return *(uint64_t*)p;
}

int main()
{
      DWORD buf[10];
      uint64_t t;

      buf[0] = 1;
      buf[1] = 2;
      t = GetIt(buf);
      buf[2] = 3;
      buf[3] = 4;

      printf ( "%d, %d, %d, %d\n", buf[0], buf[1], int(t>>32), int(t) );
      return 0;
}


Печатает оно опять же немного неожиданные циферки: 1, 2, 32573, -648193368. В отличие от предыдущего примера, где компилятор упростил функцию SwapWords() до полного отсутствия функции, здесь происходит именно перестановка местами чтения и записи. Компилятор делает вывод, что GetIt(buf) не зависит от содержимого buf, и поэтому сует «вызов» GetIt() куда считает нужным. Нужным получается вообще перед заполнением буфера.

mov    (%rsp),%r8 ; t = GetIt(buf)
...
movl   $0x1,(%rsp) ; buf[0] = 1
movl   $0x2,0x4(%rsp) ; buf[1] = 2
movl   $0x3,0x8(%rsp) ; buf[2] = 3
movl   $0x4,0xc(%rsp) ; buf[3] = 4
mov    %r8d,%r9d ; r9 = int(t)
shr    $0x20,%r8 ; r8 = int(t>>32)
callq  0x400510 <__printf_chk@plt>


В итоге в какой-нибудь переменной оказывается неверное значение (или слишком старое, или слишком новое)… и далее все вытекающие. Ловить такой подлый баг можно долго и безуспешно: для успешного проявления оптимизатор должен именно в этом «слепом» месте решить провести оптимизацию во1х, оптимизация должна проявится так, чтобы результаты поймались во2х.

Как уверенно бороться? Годных автоматических методов и тулзов не знаю. Раньше думал, что компилятор более-менее ловит; теперь однако вот знаю, что может пропустить и совершенно тривиальную конверсию, если ее слегка обернуть шаблонами (а может, и просто функциями даже). Бороться потому разве что молитвой и постом жесткой дисциплиной. Сменил указателю тип, подумай о сайд-эффектах. Почувствуй себя компилятором, подумай мальца за него: не бежит ли лиса, не летит ли орел, не ломается ли алиасинг.

Side-note: про всякие другие тонкости и фокусы из-за strict aliasing можно читать классический подробный пост по теме тов. Майка Актона, cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Что насчет MSVC?



Проблемы про strict aliasing там нет. Более того, возможности включить тоже нет. Видимо, MS приняло решение, что C99-compliant кода в мире без тонких граблей про aliasing в мире куда меньше, чем какого обычно, поэтому незачем создавать сложности. Просветительскую миссию осуществляет gcc, ну и бажок-другой иногда втихую нагенерит, не без этого.

Это автоматически означает, что фокусы про оптимизацию и __restrict для указателей там несколько важнее. Скажем, для void SumIt ( int64_t * out, const int * in, int count ) согласно strict правилу gcc имеет право «догадаться», что вряд ли out лежит в середине in; MSVC об этом догадываться гарантированно не будет. Надо либо restrict-ить вручную, либо вручную же сводить записи к минимуму. Уж локальную переменную он в регистр положить сможет.

Важно понимать, что член класса это тоже данные, лежащие по указателю this. Поэтому постоянное обращение к члену класса в цикле может компилироваться в постоянные мучения памяти.

Итого



1. Пишешь внутренний цикл, вспомни про aliasing и про __restrict.
2. Конвертируешь указатель, вспомни про strict aliasing и ядерный потенциал перестановки сайд-эффектов.
3. Пользуешься gcc, помни про дефолтный -fstrict-aliasing, не игнорируй -Wall.
4. Пользуешься MSVC, помни про принудительное отсутствие strict-aliasing-style оптимизаций, оптимизируй рукой.
5. Видишь warning насчет strict aliasing, разберись, может и UB-нуть.
6. Не видишь warning насчет strict aliasing? А он есть. Как суслик.
6.1. Компиляторы врут, ворнинги ненадежны, -Wall иногда работает (надо включать!), но гарантий не дает.
6.2. Самому себе вообще верить нельзя, только бенчмарки, дизасм, молитва и пост.
Andrew Aksyonoff @shodan
карма
376,4
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (44)

  • +1
    В русской литературе применяется слово «синонимы» для обозначения alias'ов.
    Как раз щас диплом пишу на тему анализа указателей и синонимов, не знал что gcc умеет «strict aliasing».
    • +8
      Действительно лучше использовать термин «синоним», т.к. я сначала было решил, что речь пойдет о том, чтобы проводить сглаживание при помощи C++
      • +1
        Я встречал «псевдоним».
        • 0
          К тому же,
          псевдоним = второе имя,
          синоним = слово, близкое по значению.

          Что логичнее?
        • 0
          В переведенной «Книге Дракона» используется как раз «псевдоним».
    • +1
      Как напишешь диплом, расскажешь на хабре? :)
      • +1
        Сначала надо написать :)
        • +2
          вроде полгода же всего до защиты осталось? ;-)

          я слегка удивлен, что вам не сообщили в X, что GCC умеет достаточно продвинутый анализ синонимов делать: межпроцедурный, чуствительный к потоку управления, с поддержкой указателей на поля… первая версия у них уже в 99 году появилась, в gcc 2.x. Правда GCCшники о своей реализации скорее всего никаких статей не писали.

          [впрочем и Lars Ole Andersen и Bjarne Steensgaard, и Heintze они свои анализы описывали для С или С-подобных языков, с операцией взятия адреса и т.д. Для Java все-таки чуточку проще в этом смысле… Ни указателей на поля, ни операции взятия адреса переменной. Berndl et al правда свой прикольный алгоритм с BDD для Java описали, но если я правильно понимаю ни в одном промышленном компиляторе он не используется до сих пор].
          • 0
            А я про вас слышал :)
            • 0
              надеюсь обо мне вспоминают с той же теплотой, с какой я вспоминаю родной X ;-)
        • 0
          Ну что, написал? :)
          • 0
            serious necro!
            • 0
              Я так понял, cypok продолжил в магистратуре свою бакалаврскую тему диплома, и скоро у него опять будет защита. Может, на этот раз он поделится результатами своего плодотворного труда на хабре
              • +2
                Неожиданно! :) Да, orionll все правильно вычислил. В этом году удалось достичь существенного прогресса, диплом уже выглядит солидно и содержит несколько действительно новых идей. После написания текста серьезно планирую в каком-то виде выложить это на хабр. Но надо подумать над подачей, в дипломе появилось много математики и науки. :)
                • 0
                  Ну что, дописал?
                  • 0
                    Дописать, дописал, но до публикации руки не доходят.
                    • 0
                      20 февраля будет годовщина первого комента :)
                      • 0
                        Отметили годовщину? =)
    • +2
      В профессиональной среде знакомых с явлением лиц, боюсь, слово «синоним» проще всего объяснить именно как «алиас» :) Плюс снимаются проблем неоднозначности «aliasing», «pointer aliasing» итп. Иначе «синоним указателя» например сразу имеет неоднозначный смысл — это «pointer aliasing» или таки «alias of a pointer» или еще что? Неясно.
  • +1
    Спасибо, отличная статья!
  • +7
    > Компиляторы врут

    Доктор Хаус переквалифицировался в программисты? :)
    • +1
      Это универсальная мудрость, применимая везде!
  • +1
    Удивительное рядом. Спасибо за интересную статью.
  • +1
    Отличная статья!

    Мне всегда было интересно, как же поступать в случае, если надо обработать двоичный пакет, пришедший по сети. Предположим, есть буфер char buf[12], где хранится пакет, и описание того, что в нём должно быть:
    struct packet {
      int a, b;
      char s[4];
    };

    Код типа const struct packet *p = (const struct packet *)buf; не прокатывает по вышеописанным причинам. Но, получается, всё будет хорошо, если buf скопировать в
    union {
      char buf[12];
      struct packet p;
    } u;

    и далее работать с u.p? (При условии, конечно, что данные в пакете осмысленные и не являются trap representation.)
    • +1
      есть разные подходы.
      1. передавать данные в текством виде например:
      334 27 abcd
      тут все просто — читаем два инта, и 4 символа
      istringstream istr(str);
      istr >> a >> b;
      istr.read(s, 4);


      второй вариант — бинарная передача. Тут чуть сложней
      инты у нас как известно 32 бита. Но в коде это лучше уточнить как-нибудь так
      int32 a,b;
      Никаких лонгов, или типов указателей — они будут отличаться на 64х битных платформах от интов.
      Далее — приводим их к сетевому byte-order
      int32 a_network = htonl(a);
      затем записываем в строку
      char buf[sizeof(int32)] buf;
      memcpy(buf, (char*)a_network, sizeof(int32));

      тоже самое проделываем со вторым интом — тогда у нас для отправки есть 2*sizeof(int32) байт плюс четыре байта — буфер s

      Этот метод удобен тем что мы абсолютно точно знаем сколько байт нужно принять, прежде чем начнем распаковывать сообщение. В первом случае — где целые числа записываются строкой — мы не знаем заранее сколько байт нужно получить, поэтому придется читать до какого разделителя.

      Третий вариант — не парится и использовать google-protobuf ( правда там тоже эта проблема не решена до конца, поскольку длину сообщения приходится тоже как-то передавать — что я обычно решаю передачей перед каждым сообщением 4х байт с длиной )
    • +2
      Сам спросил, сам ответил — dbp-consulting.com/StrictAliasing.pdf показывает, как это можно делать и в C, и в C++.
    • +1
      В коде про сетевой (packet*) есть много более насущные грабли. Данные туда положит системный вызов и поэтому никаких проблем с перестановкой чтений-записи из-за SA не возникнет (если сразу сконвертировать указатель и работать только с ним).

      Во1х сильно опаснее выравнивание полей (member alignment). Вероятность поймать «не такие» байтовые смещения полей куда выше, тк. компилятор поля выравнивает по границам 4/8/16 байт (и правильно делает, иначе доступ к памяти медленнее).

      Во2х плюс пересылать целые числа по сети традиционно принято в отличном от Intel порядке байт, чтоб машины итп устройства с разными порядком могли друг с другом таки говорить. Это всякие маломодные устройства типа SPARC или несколько более частые ARM унутре iPhone, iPad итп мобил. Нужно делать ntohl()/htonl().
      • –1
        А для выравнивания есть "__attribute__((__packed__))" и "#pragma pack".
        Хотя большую проблему в данном случае, имхо, создаст необходимость сначала считать данные в «упакованную структуру», а потом для скорости работы перекладывать их в «распакованный» вариант. Проще уже тогда элементы структуры читать поштучно. Ну или использовать упомянутый protobuf.
        • +1
          Основная мысль такая. Такие фокусы либо вообще не важны, либо неоправданно опасны.

          Ну те. если среда сборки и исполнения всегда одинаковая вплоть до версии компилятора, то можно смело писать вообще (packet*) и оно даже будет работать, несмотря.

          А если нет, то отличия трюков с упаковкой на разных платформах это потенциальное место для возникновения глупых проблем. Зачем такое место создавать, если можно сразу заткнуть, не очень понятно. При прочих равных предпочитаю тупой железобетонный код, который гарантированно нигде не сбойнет.
  • 0
    Месячник вкусных статей от shodan :)
    Чёрт, никогда раньше не верил, что -O3 может так легко превратить код в кровавое месиво. Как только до сих пор не нарвался…
    • 0
      Везде рекомендуют использовать максимум -O2
      • 0
        Рекомендуют, но поясняют только тем, что «оптимизации на -O3 опасны», подкрепления реальными причинами до сего момента не встречал. И до сей поры, когда собирал свой код с -O3, единственная проблема, которую встречал — это в одной библиотеке в некоторых ревизиях порой возникает уход в вечную компиляцию, причём исключительно на amd64.
        • +1
          Спрятавшийся алиасинг это довольно тонкий баг, случается не каждый год (буквально), этим как раз имхо и интересен.

          Про лютую, бешеную опасность это схоластика, конечно. Эдак можно договориться, что любые оптимизации опасны. Я и при -O1 ловил всякое. Это не более, чем лишний повод аккуратнее проверять и внятно бенчмаркать перед принятием того или иного решение.
    • 0
      Февраль выдался богатый на удивлятельные грабли. На месячник мне понадобится помощь коммьюнити в формате «расскажите мне, о чем рассказать» однако.
  • +2
    У ICC есть несколько опций управляющих работой дизамбигуатора (той части кода компилятора, которая решает как относятся друг к другу разные поинтеры).
    По умолчанию считаем что все плохо.
    -ansi-alias — прилада написана по стандарту С99
    -fno-alias — считаем что все что явно не алиаснуто свободным для оптимизаций.
  • +1
    Честно говоря, я не понимаю тягу C/C++ прогеров к максимальной оптимизации всего и вся. Как правильно сказано в статье, для «нагруженных внутренних циклов» это действительно имеет смысл, но сколько таких циклов в общем коде программы? Конечно, это зависит от приложения, но например в обычной десктопной программе их будет очень мало. И нужно ли там экономить эти 50% от 10 милисекунд? Не это ли классический пример premature optimization?

    Аналогичные чувства у меня вызывают шаблоны, когда их начинают использовать абсолютно везде, приговаривая, «ооо как клёво, компилятор сможет всё заинлайнить». Это осмысленно для низкоуровневых контейнеров, но когда дело доходит до высокоуровневых объектов все эти микро-выигрыши становятся настолько незначительными, что и говорить не о чем.

    В общем, я за хинты компилятору вроде __restrict там где это реально нужно, и против глобальных оптимизаций (таких как дефолтный -fstrict-aliasing). Так что я в каком-то смысле поддерживаю MSVC в этом вопросе :)
    • +4
      Я за точечную оптимизацию топа профайлера вообще-то. Это тоже нужно уметь делать. «Обычную десктопную программу» вообще писать на C++ сегодня не нужно. В необычных и недесктопных бывает и каждый 1% важен. Шаблоны опять-таки надо применять там, где уместно; а где неуместно, выкидывать.

      В общем, все твои чувства сводятся к одной сверхидее: инструменты нужно правильно применять. Да, нужно. Для этого инструменты нужно в первую очередь знать, во вторую уметь применять, а в третью уметь не применять.

      Конкретно про aliasing мне лично (мне лично) все равно, включен он или выключен, главное, штоп warning был. Его местами однако нет и именно это более всего удручает.
      • 0
        Не нужно-то может и не нужно, но есть куча старых программ которые нужно поддерживать…
        Другой вопрос ещё на чём писать, если не на плюсах — дотнет тоже не везде в тему.
        А к сверхидее всё можно свести, главное побольше обобщить :)
        Конечно, если всегда будет ворнинг то я ничего против не имею, но поскольку он есть не всегда — имхо уж лучше бы было выключено по умолчанию.
        • 0
          Я честно говоря не очень понимаю основного пойнта этой ветки обсуждения. О чем именно ты хочешь поговорить? Сверхидеи (см. «давайте будем богатыми и здоровыми») я так понял не годятся; дискуссиионного момента в них реально маловато, да. Хочешь пообсуждать частности, типа конкретных проблем парней, поддерживающих десктопный легаси софт, причем склонных к неуместным нано-оптимизациям? Ок, но я боюсь, на такую конкретную тему мне будет практически нечего сказать.

          Насчет «как было бы лучше» я объективного мнения не имею. Общая личная позиция такая, что пацаны из gcc шибко теоретики, иногда в ущерб практике. Конкретно в этом случае пожалуй все равно.
          • 0
            Основной пойнт я обозначил в самом начале: дефолтный -fstrict-aliasing — по моему мнению зло. Не знаю уж, есть ли тут что обсуждать :)

            Но вообще, на правах оффтопа было бы интересно узнать что ты имеешь в виду под «Обычную десктопную программу вообще писать на C++ сегодня не нужно». Я понимаю, конечно, что многие проекты сейчас выгоднее писать на .net, но если речь о чём-то нагруженном или кроссплатформенном или не обязательно требующем современной машины, то у меня мало идей что может подойти кроме плюсов.
            • 0
              А, ок. Просто заход с ламентаций про ненужные оптимизации итп шаблоны как-то смазал впечатление, что SA это именно основной!!!

              Обсуждать конструктивно нечего хотя бы просто потому, что это в конце концов стандарт. Dura lex, sed lex, gcc в своем праве.

              Для десктопа «в среднем» сегодня есть масса более приятных решений, чем лабать гуйню на, спаси господи, именно C++. Подозреваю, на любой конкретный случай жизни решение найдется. Подробно обсуждать сферический общий случай с постоянными отклонениями в частности (см. «а вдруг») не готов.
  • 0
    Я правильно понимаю, что фундаментально разные типы — это приводимые через reinterpret_cast? Классы в рамках одной иерархии считаются фундаментально разными?
    • 0
      Нет, не совсем. Для POD типов разрешен алиасинг при отличии квалификатора или знаковости (const int * супротив unsigned int * например), reinterpret тут ортогонален. К родительскому типу класса приводить тоже безопасно, конечно. Про дочерний однако не могу сказать наизусть, надо проверять; g++ -O3 -fstrict-aliasing -Wall в руки и вперед (на простейшем примере оно про type punning завсегда жалуется).
  • +1
    Нравится вот такое

    THE RESTRICT CONTRACT
    I, [insert your name], a PROFESSIONAL or AMATEUR [circle one] programmer recognize that there are limits to what a compiler can do. I certify that, to the best of my knowledge, there are no magic elves or monkeys in the compiler which through the forces of fairy dust can always make code faster. I understand that there are some problems for which there is not enough information to solve. I hereby declare that given the opportunity to provide the compiler with sufficient information, perhaps through some key word, I will gladly use said keyword and not bitch and moan about how «the compiler should be doing this for me.»

    In this case, I promise that the pointer declared along with the restrict qualifier is not aliased. I certify that writes through this pointer will not effect the values read through any other pointer available in the same context which is also declared as restricted.

    * Your agreement to this contract is implied by use of the restrict keyword ;)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.