Как я могу отклонить вызов, если во время компиляции известно граничное условие?

У меня следующая ситуация: есть огромный набор шаблонов, таких как std::vector, которые будут вызывать memmove() для перемещения частей массива. Иногда они могут захотеть "переместить" части нулевой длины - например, если хвост массива удален (как std::vector::erase()), они захотят переместить оставшуюся часть массива, которая будет иметь длину ноль, и этот ноль будет известен во время компиляции (я видел разборку - компилятор знает), но компилятор все равно будет выдавать вызов memmove().

В общем, у меня могла бы быть обертка:

inline void callMemmove( void* dest, const void* source, size_t count )
{
   if( count > 0 ) {
       memmove( dest, source, count );
   }
}

но это приведет к дополнительной проверке во время выполнения в случаях, когда count не известен во время компиляции, что мне не нужно.

Возможно ли каким-то образом использовать __assume hint чтобы указать компилятору, что, если он точно знает, что count равно нулю, он должен исключить memmove()?


person sharptooth    schedule 05.10.2011    source источник
comment
что ты надеешься этим сэкономить? Похоже на оптимизацию микро-микро-микро? Вы собираетесь сохранить две ветки (глядя на базовую реализацию memmove в GNU)?   -  person Nim    schedule 05.10.2011
comment
@Nim: ветвление, вызов memmove(), а также (наиболее важная часть), которые позволили бы оптимизировать некоторый код вокруг вызова memmove() - отсутствие вызова означает, что подготовка его аргументов не требуется. Да, он микро, но экономит микросекунды.   -  person sharptooth    schedule 05.10.2011
comment
Вы пытаетесь реализовать собственный вектор?   -  person David Rodríguez - dribeas    schedule 05.10.2011
comment
@David Rodríguez - dribeas: Вроде - для учебных целей.   -  person sharptooth    schedule 05.10.2011
comment
Да ладно, люди, у Sharptooth, кажется, достаточно опыта, чтобы знать, что преждевременная оптимизация - это корень зла и что вы не должны реализовывать свой собственный вектор, если у вас нет серьезных причин. Бывают случаи, когда есть причины для обоих, давайте предположим, что это одна из них, и давайте попробуем решить проблему, а не объявлять ее несуществующей.   -  person Suma    schedule 05.10.2011
comment
Странно, что компилятор должен послать вызов memmove, когда он уже определил длину 0. На самом деле вызов должен быть встроен, а цикл нулевого размера обнаружен и исключен. Почему этого не происходит? Вы связываетесь с динамической средой выполнения? Если да, напишите оболочку для memmove, похожую на то, что вы написали выше.   -  person Konrad Rudolph    schedule 05.10.2011
comment
@Konrad Rudolph: AFAIK причина в том, что memmove() реализован в сборке в источниках среды выполнения Visual C ++ и не представлен компилятору.   -  person sharptooth    schedule 05.10.2011
comment
@KonradRudolph memcpy встроен (на самом деле встроен), и компилятор достаточно умен, чтобы его устранить, а memmove - нет.   -  person Suma    schedule 05.10.2011
comment
@Suma Хм. По какой причине? Я понимаю, что это, вероятно, реализовано на ассемблере, но тогда оптимизация времени компоновки должна позаботиться о необходимом встраивании.   -  person Konrad Rudolph    schedule 05.10.2011
comment
@KonradRudolph Время компоновки CG не может оптимизировать функции сборки. Невозможно даже встраивание функции сборки. 1) вы не можете изменить способ передачи аргументов, 2) функции уже заканчиваются на ret или, возможно, с несколькими rets, нет надежного способа обрезать это ret. Реализация memcpy совершенно другая, она не только встроена, она обрабатывается компилятором как внутренняя функция, и компилятор может использовать все, что он знает, чтобы решить, как ее скомпилировать.   -  person Suma    schedule 05.10.2011
comment
возможный дубликат обнаружения константы времени компиляции C ++   -  person Suma    schedule 05.10.2011
comment
@Suma Оптимизация времени компоновки переписывает код (это необходимо для встраивания!). Я не понимаю, чем это отличается от C ++ к сборке. Оба являются просто объектными файлами (с дополнительной информацией). Если, конечно, VC ++ не предоставляет подходящие дистрибутивы библиотек. Это было бы глупо.   -  person Konrad Rudolph    schedule 05.10.2011
comment
@KonradRudolph Он сильно отличается, и есть дополнительная информация. LTCG работает не с собственным кодом, а с символьным представлением кода (поэтому его нужно включать также при компиляции объектов, а не только при компоновке). Это невозможно сделать из сборки. См., Например, msdn.microsoft.com/en-us/magazine/cc301698.aspx для получения дополнительной информации.   -  person Suma    schedule 05.10.2011


Ответы (4)


Смысл __assume в том, чтобы указать компилятору пропускать части кода при оптимизации. В предоставленной вами ссылке пример дается с предложением default конструкции switch - там подсказка сообщает компилятору, что предложение никогда не будет достигнуто, даже если теоретически это возможно. Вы обычно говорите оптимизатору: «Эй, я знаю лучше, выбросьте этот код».

Для default вы не можете не записывать его (если вы не покрываете весь диапазон в cases, что иногда бывает проблематично), потому что это вызовет ошибку компиляции. Итак, вам нужна подсказка для оптимизации кода, вы знаете, что в нем нет необходимости.

В вашем случае - код доступен, но не всегда, поэтому подсказка __assume вам не сильно поможет. Вы должны проверить, действительно ли count равно 0. Если вы не уверены, что оно никогда не может быть чем-либо, кроме 0, просто не записывайте его.

person littleadv    schedule 05.10.2011

В этом решении используется трюк, описанный в Обнаружении константы времени компиляции C ++ - трюк использует Фактическое целое число времени компиляции, равное нулю, можно преобразовать в указатель, и это можно использовать вместе с перегрузкой для проверки свойства «известное время компиляции».

struct chkconst {
  struct Small {char a;};
  struct Big: Small {char b;};
  struct Temp { Temp( int x ) {} };
  static Small chk2( void* ) { return Small(); }
  static Big chk2( Temp  ) { return Big(); }
};

#define is_const_0(X) (sizeof(chkconst::chk2(X))<sizeof(chkconst::Big))
#define is_const(X) is_const_0( int(X)-int(X) )

#define memmove_smart(dst,src,n) do { \
    if (is_const(n)) {if (n>0) memmove(dst,src,n);} \
    else memmove(dst,src,n); \
  } while (false)

Или, в вашем случае, поскольку вы все равно хотите проверять только ноль, можно использовать is_const_0 напрямую для максимальной простоты и переносимости:

#define memmove_smart(dst,src,n) if (is_const_0(n)) {} else memmove(dst,src,n)

Примечание: здесь используется версия is_const более простая, чем в связанном вопросе. Это связано с тем, что в этом случае Visual Studio более соответствует стандартам, чем GCC. Если нацелен на gcc, вы можете использовать следующий вариант is_const (адаптированный для обработки всех возможных целочисленных значений, включая отрицательные и INT_MAX):

#define is_const_0(X) (sizeof(chkconst::chk2(X))<sizeof(chkconst::Big))
#define is_const_pos(X) is_const_0( int(X)^(int(X)&INT_MAX) )
#define is_const(X) (is_const_pos(X)|is_const_pos(-int(X))|is_const_pos(-(int(X)+1)))
person Suma    schedule 05.10.2011
comment
Во всех операциях, в которых используются внешние итераторы, реализация не может знать, что переданные итераторы не из одного и того же контейнера, и поэтому вы не можете использовать memcpy, и то же самое происходит с erase (если вы удалите элемент в середине и есть если больше двух элементов, диапазоны гарантированно перекрываются). С другой стороны, вы можете использовать memcpy при увеличении буфера, так как это гарантирует, что источник и назначения не перекрываются. - person David Rodríguez - dribeas; 05.10.2011
comment
Нет, это обычное разрешение функции, даже шаблона нет. x (void * a) используется, когда value является нулевой константой, x (Temp a) в противном случае (Temp может быть построен из int, но это не предпочтительная перегрузка для нуля). Я тоже считаю это супер-крутым. Первоначальным источником идеи, по-видимому, является encode.ru/threads/396 -C-compile-time-constant-detection - person Suma; 06.10.2011
comment
@sharptooth Это не SFINAE, но потенциально может использоваться в SFINAE. Что касается ответа, я думаю, что это круто, но я действительно не понимаю, как это помогает с проблемой. Предпосылка (как я понял) состоит в том, что компилятор не удалял if (count > 0) в случае, когда было известно во время компиляции как 0, как изменение compile_time_constant_0 > 0 на sizeof(X) > sizeof(Y) влияет на то, как компилятор генерирует код? (Предполагая, что 0 известен во время компиляции, оба должны быть одинаково легко оптимизированы) Следующий вопрос будет заключаться в том, как Sharptooth пришел к такому выводу ... - person David Rodríguez - dribeas; 06.10.2011
comment
@ DavidRodríguez-dribeas Нет, проблема была в другом - когда использовалось условие, if (count ›0) и memmove были пропущены правильно для известного нуля, но if все еще оставался там, когда значение не было известно время компиляции, представляя ненужные накладные расходы . В этом решении отсутствуют накладные расходы, но исключаются нулевые перемещения. - person Suma; 06.10.2011
comment
Почему версия gcc более сложная? - person Adrian; 18.05.2016

Я думаю, что вы неправильно поняли значение __assume. Он не сообщает компилятору об изменении своего поведения, когда он знает, каковы значения, а скорее сообщает ему, какими будут значения, когда он не может вывести его самостоятельно.

В вашем случае, если вы сказали __assume, что count > 0 он пропустит тест, поскольку вы уже сказали ему, что результат всегда будет true, он удалит условие и вызовет memmove always, который это именно то, чего вы хотите избежать.

Я не знаю особенностей VS, но в GCC есть вероятный / маловероятный внутренний (__builtin_expect((x),1)), который можно использовать для подсказки компилятору относительно того, какой наиболее вероятный исход теста. при этом тест не удаляется, но код компоновки будет таким, чтобы наиболее вероятная (как в по вашему определению) ветвь была более эффективной (не разветвлялась).

person David Rodríguez - dribeas    schedule 05.10.2011
comment
В VS нет ничего похожего на вероятность / маловероятность, и это довольно печально. - person sharptooth; 05.10.2011
comment
Я, кажется, припоминаю, что по умолчанию предполагается, что берется первая ветвь (if), что означает, что если это наименее ожидаемая ветвь, вы можете повлиять на сгенерированный код, вернув условие и предложения if / else - person David Rodríguez - dribeas; 05.10.2011
comment
@ Дэвид Родригес - dribeas: Этот трюк не работает, когда у вас if без else. - person sharptooth; 05.10.2011
comment
if (not condition) {} else { body }? Или вы имеете в виду, что компилятор сгенерирует тот же код, что и в if (condition) { body }? С точки зрения местоположения кода они будут генерировать одно и то же, код будет там, где есть if, и будет переход до конца, но я не уверен, будет ли фактический тест / прыжок таким же и / или будет ли процессор обрабатывать его по-другому - person David Rodríguez - dribeas; 05.10.2011
comment
@ Дэвид Родригес - dribeas: Компилятор действительно сгенерирует один и тот же код для if без else и для if-else с пустой веткой if. - person sharptooth; 05.10.2011

Если возможно переименовать memmove, я думаю, что подойдет что-то вроде этого - http://codepad.org/s974Fp9k

struct Temp {
  int x;
  Temp( int y ) { x=y; }
  operator int() { return x; };
};

void memmove1( void* dest, const void* source, void* count ) {
  printf( "void\n" );
}

void memmove1( void* dest, const void* source, Temp count ) {
  memmove( dest, source, count );
  printf( "temp\n" );
}

int main( void ) {
  int a,b;
  memmove1( &a,&b, sizeof(a) );
  memmove1( &a,&b, sizeof(a)-4 );
}

Я думаю, что то же самое, вероятно, возможно и без класса - нужно посмотреть правила преобразования, чтобы подтвердить это.

Также должна быть возможность перегрузить исходный memmove (), например. передавая объект (например, Temp (sizeof (a)) в качестве третьего аргумента.

Не уверен, какой способ удобнее.

person Shelwien    schedule 08.10.2011