Безопасное использование memcpy в перекрывающейся области

Безопасно ли использовать memcpy в следующем сценарии, когда данные копируются из большего индекса в блок в меньший индекс в том же блоке. Например:

char buf[100];
// fill in the data ...
memcpy(&buf[10], &buf[15], 10);

В приведенном выше сценарии меня не интересуют данные из ячеек 10–19, и я не против, если они будут перезаписаны. Есть ли какая-то причина, по которой этого следует избегать и вместо этого использовать memmove?

РЕДАКТИРОВАТЬ: извините, я не сообщил о своем намерении должным образом, поэтому скажем, у меня есть данные из индекса 10–19 и данные из индекса 15–24, я хочу скопировать данные из 15–24 поверх 10–19, и мне все равно о данных с 10 по 19, безопасно ли для нас memcpy даже если они перекрываются?


person Abhas Saroha    schedule 18.01.2014    source источник
comment
это не перекрывается - memcpy(&buf[20], &buf[10], 20); будет   -  person user3125280    schedule 18.01.2014
comment
Извините, я имел в виду наоборот, исправил выше.   -  person Abhas Saroha    schedule 18.01.2014
comment
Если A и B не пересекаются, то B и A также не пересекаются...   -  person Oliver Charlesworth    schedule 18.01.2014
comment
Если есть перекрытие, поведение memcpy() не определено. Для memmove() гарантируется правильное поведение.   -  person wildplasser    schedule 18.01.2014
comment
@ wildplasser это потому, что порядок, в котором копируется каждый байт, не определен?   -  person Abhas Saroha    schedule 18.01.2014
comment
@ user689046: Да - запись в буфер назначения может изменить необходимые, но еще не прочитать данные в буфере источника, если источник и место назначения перекрываются   -  person Brendan    schedule 18.01.2014
comment
@Брендан, спасибо, это имеет смысл.   -  person Abhas Saroha    schedule 18.01.2014


Ответы (3)


Изменить:

На основе вашего редактирования инвертируйте следующий ответ, поскольку теперь вы не согласны с ограничениями restrict.


Старый ответ

Да, это безопасно. Вы копируете с buf[10] по buf[19] на buf[20] по buf[29]. (Обратите внимание, что первым параметром memcpy является пункт назначения. Таким образом, с buf[10] по buf[19] не перезаписывается.)

memcpy определяется в C11 как:

void *memcpy(void * restrict s1, const void * restrict s2, size_t n);

обратите внимание на ключевое слово restrict. C11 в 6.7.3.8 говорит (выделено мной):

Объект, доступ к которому осуществляется через указатель с ограничением, имеет специальную связь с этим указателем. Эта ассоциация, определенная в 6.7.3.1 ниже, требует, чтобы все обращения к этому объекту прямо или косвенно использовали значение этого конкретного указателя. класс хранения) должен способствовать оптимизации, и удаление всех экземпляров квалификатора из всех единиц трансляции предварительной обработки, составляющих соответствующую программу, не меняет его значения (т. е. наблюдаемого поведения).

В вашем примере с buf[10] по buf[19] осуществляется доступ только через указатель s2, а с buf[20] по buf[29] — только с помощью указателя s1. Поэтому ваше использование memcpy совершенно нормально.

Проще говоря, пока массивы, которые вы передаете memcpy, не перекрываются, все в порядке.

person Shahbaz    schedule 18.01.2014
comment
Извините, см. редактирование выше, безопасно ли, если мне не нужны более ранние данные в перекрывающихся регионах и я перезаписываю их более поздними данными? - person Abhas Saroha; 18.01.2014
comment
В случае перекрывающихся областей это небезопасно, потому что нет гарантии порядка, в котором данные будут скопированы? В случае, если индекс j (j › i && j ‹ i + data_length) копируется в i до j + 1 в i + 1, это должно быть совершенно безопасно. - person Abhas Saroha; 18.01.2014
comment
@ user689046, нет гарантии, в каком направлении будут скопированы данные, поэтому вы можете получить полностью поврежденные данные. Вот почему memmove существует. - person Shahbaz; 18.01.2014
comment
Спасибо, что проясняете для меня вещи. - person Abhas Saroha; 18.01.2014
comment
Было бы чтение значения с использованием одного указателя ограничения, за которым следует запись этого значения с использованием другого вызова UB, если бы не было возможности, чтобы запись могла предшествовать чтению [например, код *p=*q; с равными p и q]? Есть случаи, когда определение memcpy(p,p,n); как копирование любого или всех значений в себя в произвольном порядке позволило бы писать код более эффективно, чем если бы это рассматривалось как Undefined Behavior. - person supercat; 31.10.2015
comment
@supercat, да, это будет UB. Это просто restrict и независимо от того, всегда ли это хорошая идея или нет, это то, что требуется. Мне любопытно узнать, в какой ситуации вам вообще может понадобиться memcpy(p, p, n)! - person Shahbaz; 01.11.2015
comment
@Shahbaz: Некоторые алгоритмы сортировки, перетасовки или другие связанные с перестановкой алгоритмы могут быть написаны наиболее эффективно, если им разрешено обмениваться элементами друг с другом. Если копируемые элементы имеют небольшой размер (иногда правила псевдонимов вынуждают использовать memcpy с 4-байтными типами) и лишь малая часть свопов является рефлексивной, может оказаться более эффективным слепо поменять местами несколько элементов, чем замедлять их работу. общий случай, чтобы избежать таких самозамен. Учитывая, что основная цель ограничения состояла в том, чтобы позволить указателям жадно читаться и писаться лениво, я думаю, что это неудачно... - person supercat; 01.11.2015
comment
... что стандарт не определяет поведение в случаях, когда либо (1) записываемые данные соответствуют тому, что уже было, либо (2) зависимость данных между операциями чтения и записи гарантирует, что все операции чтения по одному указателю будут предшествовать любой записи другим. По иронии судьбы, я бы подумал, что в 1999 году, если бы кто-то предложил это, я думаю, что эта идея могла бы быть отвергнута не на том основании, что она будет препятствовать оптимизации, а скорее на том основании, что не было необходимости чрезмерно усложнять спецификацию, чтобы предотвратить компиляторы. делать что-то, что у них не было бы причин делать в любом случае. - person supercat; 01.11.2015
comment
@supercat, если предположить, что в ваших примерах действительно имеет значение возможность обменивать несколько небольших элементов массива, меня все равно не убеждает, что restrict должен содержать такие особые случаи. Кстати, в ваших примерах вы могли бы использовать memmove вместо memcpy. Тем не менее, restrict играет важную роль для оптимизатора, сообщая ему, что он может с уверенностью предположить, что то, что читается/записывается через какой-то указатель, не будет изменяться через какой-либо другой указатель. Дело не обязательно в порядке чтения и записи. - person Shahbaz; 02.11.2015
comment
@Shahbaz: Код, который ожидает, что memcpy от адреса к самому себе не должен иметь побочных эффектов, существовал практически всегда, и я не вижу ничего, что можно было бы получить, нарушив его, учитывая количество способов, которыми Стандарт мог бы его приспособить. Например, независимо от того, что означает ограничение, стандарт можно изменить, чтобы сказать, что memcpy(p,p,n); может читать и записывать любую часть диапазона в любом порядке, используя указатель источника, а также может читать и записывать любую непересекающуюся часть диапазона. в любом порядке, используя указатель назначения. Такое поведение сделало бы такой код, как я описал... - person supercat; 02.11.2015
comment
...работает, но не ограничивает никакие оптимизации, кроме тех, которые ошибочно предполагают, что источник и место назначения не могут быть равны. Если в противном случае полезная возможность оптимизации будет упущена, программист, желающий включить ее, может использовать подходящую встроенную компилятор, чтобы предложить компилятору предположить, что указатели не могут быть равными. Многие компиляторы не оптимизируют memmove так же хорошо, как memcpy (в некоторых реализациях функция перестановки, использующая memmove, может работать вдвое медленнее, чем функция, использующая memcpy), и использование memmove не особенно привлекательно. - person supercat; 02.11.2015
comment
@supercat, зачем стандартный C, который memcpy(p, p, n) может копировать в любом порядке. Не лучше ли сказать, что memcpy(p, p, n) вообще ничего не делает? - person Shahbaz; 02.11.2015
comment
@Shahbaz: Если сказать, что он вообще ничего не делает, это будет означать, что вызов memcpy(p,p,n) в случаях, когда чтение и запись одного и того же значения в p может иметь неблагоприятные наблюдаемые побочные эффекты, не приведет к таким побочным эффектам. Сказать, что он может читать и записывать любую часть исходного диапазона и любую часть диапазона назначения, позволит ему ничего не делать, но не потребует от компилятора или библиотеки делать что-либо, кроме той же последовательности операций. это произошло бы без квалификатора limited . Кстати, какие формы полезной оптимизации... - person supercat; 02.11.2015
comment
... вы видите, что restrict был разработан для облегчения, кроме возможности выполнять жадное чтение и ленивую запись? Нетерпеливое чтение и ленивая запись полезны, но требование самозапоминающейся работы никоим образом не помешает им, поскольку запись не может произойти до последней точки, когда может произойти чтение. Кстати, встроенная функция, которая на самом деле была бы на удивление полезной в C, будет вести себя как memmove(p,p,n), но не должна генерировать какой-либо код, а просто вести себя для целей псевдонимов типов, как если бы произошло memmove, поскольку... - person supercat; 02.11.2015
comment
... если бы такая встроенная функция существовала, много кода, который в противном случае использовал бы глупые маленькие операции memcpy, мог бы использовать эту встроенную функцию, а затем использовать каламбур типа на основе указателя так, как это было распространено до того, как C89 фактически объявил его вне закона. - person supercat; 02.11.2015
comment
Re: вызов memcpy(p,p,n) в случаях, когда чтение и запись одного и того же значения в p может иметь неблагоприятные наблюдаемые побочные эффекты, не приведет к таким побочным эффектам, я довольно уверен, что если вы напишете *p = *p, он будет оптимизирован. Единственный способ предотвратить это — использовать квалификатор volatile. Вы не можете передать volatile указатели на mempy, поэтому эта точка отключена. - person Shahbaz; 02.11.2015
comment
Re: какие формы полезной оптимизации вы видите... Представьте себе это: *p = x; *q = y; *(p+1) = *p + 1; (Обратите внимание, что это не обязательно фактический код, а последовательность действий, возможно, например, вырванная из цикла ). Если p равно restrict, компилятор может увеличить регистр, в котором хранится x, и записать его в *(p+1). Если это не restrict, то он должен прочитать обратно *p, потому что *q = y могло измениться *p. - person Shahbaz; 02.11.2015
comment
Re: встроенная функция, которая на самом деле оказалась бы на удивление полезной в C, была бы чем-то, что вело бы себя как memmove(p,p,n), но не ожидалось, что оно будет генерировать какой-либо код, а просто вело бы себя для целей псевдонимов типов, как если бы произошло memmove. Я не уверен, что понял это, ни то, какие маленькие глупые memcpy операции он предотвращает, ни то, что было распространено до C89. - person Shahbaz; 02.11.2015
comment
@Shahbaz: до C89, если несколько типов структур (возможно, в разных единицах компиляции) имели некоторые общие начальные члены, метод, который принимал массив указателей на один такой тип и обращался только к вышеупомянутым начальным членам, мог безопасно получить подходящим образом приведенный массив указателей на любой такой тип. Чрезмерно широкое правило (IMHO) в C89 гласит, что программа, которая использует тип foo** для чтения памяти, записанной как type bar**, вызывает Undefined Behavior, даже если все применимые схемы хранения будут гарантированно совместимы. Единственный портативный обходной путь - это... - person supercat; 02.11.2015
comment
...используйте memcpy для указателя. Исторически сложилось так, что немногие программисты обращались бы к memcpy для таких вещей, потому что компиляторы просто транслировали бы код простым и понятным способом. Однако сегодня некоторые компиляторы придерживаются мнения, что компиляторы должны приложить все усилия, чтобы удалить код, который обрабатывает только сценарии, в которых Стандарт не налагает никаких требований. Таким образом, использование типа foo** для чтения чего-то, что могло быть написано как какой-то другой, возможно, неизвестный, но совместимый с макетом тип указателя, больше не является безопасным, и вместо этого нужно использовать глупые маленькие 4-байтовые операции memcpy/memmove. - person supercat; 02.11.2015
comment
Альтернативой использованию memcpy/memmove для копирования указателя в локальную переменную было бы использование memmove для копирования указателя на себя, что заставило бы его вести себя так, как если бы он был написан с использованием unsigned char*, который освобожден от обычных правил псевдонимов. , но для этого сценария memmove не должен физически что-либо делать — все, что ему нужно сделать, это не дать компилятору сделать какие-либо выводы на основе типов, используемых для чтения и записи памяти. - person supercat; 02.11.2015

Как вы указали в заявлении:

memcpy(&buf[20], &buf[10], 10);

Данные из индекса с 10 по 19 не перекрываются с данными из индекса с 20 по 29, поэтому безопасно использовать memcpy(), даже если вам нужны данные из индекса с 10 по 19.

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

person rullof    schedule 18.01.2014
comment
извините, я перепутал пункт назначения и источник, исправил выше! - person Abhas Saroha; 18.01.2014
comment
спасибо, что порядок копирования не определен, это проясняет для меня. - person Abhas Saroha; 18.01.2014

Это безопасно. Нет причин использовать memmove.

memcpy не указывает направление, поэтому memcpy(&buf[20], &buf[10], 20); может ввести в заблуждение. мы должны убедиться, что копия начинается с &buf[20]. memmove и std::copy действительно дают такие гарантии, поэтому их можно безопасно использовать в таком случае.

memcpy(&buf[10], &buf[20], 10); не перекрывается, потому что &buf[10] + 9 == &buf[19] — это скопированный адрес make, который меньше &buf[20].

person user3125280    schedule 18.01.2014
comment
Вы сказали, что это небезопасно, а потом сказали, что данные не пересекаются. ваш ответ сбивает с толку! - person rullof; 18.01.2014
comment
@EricPostpischil хм, хорошо, я отвечал на вопросительный знак. Легко исправить. - person user3125280; 18.01.2014