char *stpcpy(char *dest, const char *src);
возвращает указатель на конец строки и является частью POSIX.1-2008 .До этого это было расширение GNU libc с 1992 года. Если впервые появилось в Lattice C AmigaDOS в 1986 году.
gcc -O3
в некоторых случаях оптимизирует strcpy
+ strcat
для использования stpcpy
или strlen
+ встроенное копирование, см. Ниже.
Стандартная библиотека C была разработана очень рано, и очень легко утверждать, что функции str*
разработаны не оптимально.Функции ввода / вывода были определенно разработаны очень в начале, в 1972 году, еще до того, как в С даже появился препроцессор, а именно , почему fopen(3)
принимает строку режима вместо битовой карты флага, такой как Unix open(2)
.
Мне не удалось найти список функций, включенных в «пакет переносимого ввода-вывода» Майка Леска, поэтому я не знаю, датирует ли strcpy
в его текущей форме всевернуться туда или если эти функции были добавлены позже.(Единственный реальный источник, который я нашел, - это широко известная статья C History по Деннису Ритчи , которая превосходна, но не , которая глубже. Я не нашел ни документации, ни исходного кодадля самого пакета ввода / вывода.)
Они появляются в их текущем виде в K & R first edition , 1978.
Функции должнывернуть результат вычислений, которые они делают, если он потенциально полезен для вызывающего, вместо того, чтобы выбрасывать его .Либо как указатель на конец строки, либо как целочисленная длина.(Указатель был бы естественным.)
Как говорит @R:
Мы все хотим, чтобы эти функции возвращали указатель на завершающий нулевой байт (который уменьшил бы * 1046)* операции с O(1)
)
например, вызов strcat(bigstr, newstr[i])
в цикле для построения длинной строки из множества коротких (O (1) длины) строк имеет приблизительно O(n^2)
сложность, но strlen
/ memcpy
будет смотреть только на каждый символ дважды (один раз в strlen, один раз в memcpy).
Используя только стандартную библиотеку ANSI C, невозможно эффективно просматривать только каждый символ один раз .Вы можете вручную написать цикл byte-a-a-time, но для строк длиннее нескольких байтов это хуже, чем смотреть на каждый символ дважды с помощью текущих компиляторов (которые не будут автоматически векторизовывать цикл поиска) в современном HW,предоставлены эффективные предоставляемые libc SIMD strlen и memcpy.Вы можете использовать length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;
, но sprintf()
должен разобрать строку формата и не быстро.
Нет даже версии strcmp
или memcmp
, который возвращает позицию разницы .Если это то, что вам нужно, у вас та же проблема, что и у Почему сравнение строк так быстро в python? : оптимизированная библиотечная функция, которая работает быстрее, чем все, что вы можете сделать с помощью скомпилированного цикла (если у вас нетоптимизированный asm для каждой целевой платформы, которая вас интересует), которую вы можете использовать, чтобы приблизиться к отличающемуся байту, прежде чем вернуться к обычному циклу, как только вы приблизитесь.
Кажется, что строковая библиотека C быларазработан без учета стоимости O (n) любой операции, а не просто для нахождения конца строк неявной длины, и поведение strcpy
определенно не единственный пример.обрабатывать строки неявной длины как целые непрозрачные объекты, всегда возвращая указатели в начало, никогда в конец или в позицию внутри единицы после поиска или добавления.
догадка истории
В раннем C на PDP-11 я подозреваю, что strcpy
был не более эффективным, чем while(*dst++ = *src++) {}
(и, вероятно, был реализован, чтоау).
На самом деле, K & R first edition (стр. 101) показывает, что реализация strcpy
и говорит:
Хотя это может показаться загадочным вНа первый взгляд, удобство обозначений значительно, и идиома должна быть освоена, если только по той причине, что вы часто будете ее видеть в программах на Си.
Это означает, что они полностью ожидали, что программисты будут писать свои собственные циклы в тех случаях, когда вам нужно конечное значение dst
или src
. И поэтому, может быть, они не видели необходимости перепроектировать стандартный библиотечный API, пока не стало слишком поздно, чтобы представить более полезные API для оптимизированных вручную функций библиотеки asm.
Но имеет ли смысл возвращать исходное значение dst
? 1108 *
strcpy(dst, src)
возврат dst
аналогичен x=y
с оценкой x
. Так что он заставляет strcpy работать как оператор присваивания строки.
Как указывают другие ответы, это позволяет вкладывать, как foo( strcpy(buf,input) );
. Ранние компьютеры были очень ограничены в памяти. Сохранение компактности вашего исходного кода было обычной практикой . Перфокарты и медленные терминалы, вероятно, были фактором в этом. Я не знаю исторических стандартов кодирования, руководств по стилю или того, что считалось слишком много, чтобы поставить его в одну строку.
Хрустящие старые компиляторы также могли быть фактором. С современными оптимизирующими компиляторами char *tmp = foo();
/ bar(tmp);
не медленнее, чем bar(foo());
, но с gcc -O0
. Я не знаю, могли ли бы очень ранние компиляторы полностью оптимизировать переменные (не резервируя для них место в стеке), но, надеюсь, они могли бы, по крайней мере, хранить их в регистрах в простых случаях (в отличие от современного gcc -O0
, который специально разливает / перезагружает все для последовательная отладка). то есть gcc -O0
не является хорошей моделью для древних компиляторов, потому что антиоптимизирует с целью последовательной отладки.
Возможная мотивация, сгенерированная компилятором
Учитывая недостаточную заботу об эффективности в общем дизайне API библиотеки C-строк, это может быть маловероятным. Но, возможно, было преимущество размера кода. (На ранних компьютерах размер кода был более жестким ограничением, чем время процессора).
Я не знаю много о качестве ранних компиляторов Си, но можно с уверенностью сказать, что они не были удивительными в оптимизации, даже для хорошей простой / ортогональной архитектуры, такой как PDP-11.
Обычно требуется указатель строки после вызова функции. На уровне asm у вас (компилятора), вероятно, есть это в регистре перед вызовом. В зависимости от соглашения о вызовах, вы либо помещаете его в стек, либо копируете его в правильный регистр, где соглашение о вызовах говорит, что первый аргумент идет. (то есть где strcpy
ожидает этого). Или, если вы планируете заранее, у вас уже был указатель в правильном регистре для соглашения о вызовах.
Но функция вызывает некоторые регистры, включая все регистры передачи аргументов. (Таким образом, когда функция получает аргумент в регистре, она может увеличивать его там вместо копирования в пустой регистр.)
Таким образом, в качестве вызывающей стороны ваша опция генерации кода для сохранения чего-либо в вызове функции включает в себя:
- сохранить / перезагрузить его в локальную память стека. (Или просто перезагрузите его, если последняя копия все еще находится в памяти).
- сохранить / восстановить сохраненный вызовом регистр в начале / конце всей вашей функции и скопировать указатель на один из этих регистров перед вызовом функции.
- функция возвращает значение в регистр для вас. (Конечно, это работает только в том случае, если источник C написан для использования возвращаемого значения вместо входной переменной. Например,
dst = strcpy(dst, src);
, если вы его не вкладываете).
Все соглашения о вызовах для всех архитектур. Мне известно о возвращаемых возвращаемых значениях размера указателя в регистре, поэтому наличие, возможно, одной дополнительной инструкции в библиотечной функции может сохранить размер кода для всех вызывающих, которые хотят использовать это возвращаемое значение.
Вы, вероятно, получили лучший ассемблер от примитивных ранних компиляторов Си, используя возвращаемое значение strcpy
(уже в регистре), чем заставляя компилятор сохранять указатель вокруг вызова в регистре с сохранением вызова или проливать его на стек. Это все еще может иметь место.
Кстати, на многих ISA регистр возвращаемых значений не является первым регистром передачи аргументов.И если вы не используете режимы адресации base + index, для strcpy потребуется дополнительная инструкция (и связать другой регистр) для копирования регистра для цикла приращения указателя.
Набор инструментов PDP-11 обычноиспользовал какое-то соглашение о вызове стековых аргументов , всегда помещающее аргументы в стек.Я не уверен, сколько регистров с сохраненным вызовом и с закрытым вызовом было нормальным, но было доступно только 5 или 6 регистров GP ( R7 - счетчик программы, R6 - указатель стека, R5 часто используется как фреймуказатель ).Так что он похож на 32-битный x86, но даже более тесен.
char *bar(char *dst, const char *str1, const char *str2)
{
//return strcat(strcat(strcpy(dst, str1), "separator"), str2);
// more readable to modern eyes:
dst = strcpy(dst, str1);
dst = strcat(dst, "separator");
// dst = strcat(dst, str2);
return dst; // simulates further use of dst
}
# x86 32-bit gcc output, optimized for size (not speed)
# gcc8.1 -Os -fverbose-asm -m32
# input args are on the stack, above the return address
push ebp #
mov ebp, esp #, Create a stack frame.
sub esp, 16 #, This looks like a missed optimization, wasted insn
push DWORD PTR [ebp+12] # str1
push DWORD PTR [ebp+8] # dst
call strcpy #
add esp, 16 #,
mov DWORD PTR [ebp+12], OFFSET FLAT:.LC0 # store new args over our incoming args
mov DWORD PTR [ebp+8], eax # EAX = dst.
leave
jmp strcat # optimized tailcall of the last strcat
Это значительно более компактно, чем версия, в которой не используется dst =
, и вместо этого повторно используется входной аргумент для strcat
.(См. Оба в проводнике компилятора Godbolt .)
Вывод -O3
очень отличается: gcc для версии, которая не использует возвращаемое значение, используетstpcpy
(возвращает указатель на хвост), а затем mov
- немедленно, чтобы сохранить данные строковых литералов прямо в нужном месте.
Но, к сожалению, версия dst = strcpy(dst, src)
-O3 все еще использует обычные strcpy
, затем встраивает strcat
как strlen
+ mov
-mmediate.
В C-строку или не в C-строку
C строки неявной длины arenне всегда изначально плохо и имеет интересные преимущества (например, суффикс также является допустимой строкой, без необходимости ее копировать).
Но библиотека строк C не разработана таким образом,это делает возможным эффективный код, потому что циклы char
в-времени обычно не векторизуются автоматически, а библиотечные функции отбрасывают результаты работы, которую они должны выполнить.
gcc и clang никогда не автоматическивекторизовать циклы, если число итераций не известно до первогонапример, for(int i=0; i<n ;i++)
.ICC может векторизовать поисковые циклы, но все равно вряд ли справится так же, как и рукописный asm.
strncpy
и так далее, по сути, катастрофа .например, strncpy
не копирует завершающий '\0'
, если он достигает предела размера буфера.Кажется, он предназначен для записи в середину больших строк, , а не для предотвращения переполнения буфера.Не возвращая указатель в конец означает, что вам нужно arr[n] = 0;
до или после, потенциально касаясь страницы памяти, которую никогда не нужно было трогать.
Некоторые функции, такие как snprintf
, могут быть использованы и всегда nul-terminate.Запоминание того, что делает, что трудно, и огромный риск, если вы помните неправильно, поэтому вы должны проверять каждый раз, в каких случаях это имеет значение для правильности.
Как говорит Брюс Доусон: Прекратите использовать strncpy уже!.Очевидно, что некоторые расширения MSVC, такие как _snprintf
, еще хуже.