В моем исходном коде я увидел странное поведение компилятора руки, когда он выполнял избыточную итерацию по строке, что не нужно. Я показываю здесь минимальный пример, который показывает это, и задаю свой вопрос ниже этого
#include <string.h>
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
int MAX_FILE_NAME = 2500;
int F(char *file){
int file_len = MIN(strlen(file), MAX_FILE_NAME - 1);
return file_len;
}
int main(void) {
F(__FILE__);
return 0 ;
}
Скомпилировано с:
arm-none-eabi-gcc -nostdlib -Xlinker -Map = "m7_experiments.map" -Xlinker --cref -Xlinker --gc-sections -Xlinker -print-memory-usage -mcpu=cortex-m7 -mfpu=fpv5-sp-d16 -mfloat-abi=hard -mthumb -T "m7_experiments_Debug.ld" -o "m7_experiments.axf" ./src/cr_startup_cm7.o ./src/crp.o ./src/flashconfig.o ./src/m7_experiments.o
Приводит к:
Dump of assembler code for function F:
0x00000104 <+0>: push {r4, lr}
0x00000106 <+2>: mov r4, r0
0x00000108 <+4>: bl 0x13c <strlen>
0x0000010c <+8>: mov r2, r0
0x0000010e <+10>: ldr r3, [pc, #20] ; (0x124 <F+32>)
0x00000110 <+12>: ldr r0, [r3, #0]
0x00000112 <+14>: subs r0, #1
0x00000114 <+16>: cmp r2, r0
0x00000116 <+18>: bcc.n 0x11a <F+22>
0x00000118 <+20>: pop {r4, pc}
0x0000011a <+22>: mov r0, r4
0x0000011c <+24>: bl 0x13c <strlen>
0x00000120 <+28>: b.n 0x118 <F+20>
0x00000122 <+30>: nop
0x00000124 <+32>: lsls r0, r3, #6
0x00000126 <+34>: movs r0, r0
Обратите внимание, как в случае, если длина файла короче определенной, вместо того, чтобы просто получить ее длину из $r2, она вычисляется снова, что ухудшает время выполнения до 2 * длины файла. что кажется ненужным. Есть ли способ оправдать поведение компилятора в этом случае? Мне интересно знать.
Вы компилируете без оптимизации. Почему вы ожидаете оптимизированный код?
Нужно ДВ. Какой будет результат MIN(x++)?
@fuz в моем реальном примере кода это происходит с O3. также можно подумать, что некоторые оптимизации актуальны даже для процесса компиляции по умолчанию.
@e.ad Без оптимизации компилятор может и будет генерировать настолько глупый код, насколько ему нравится. Не ждите никаких оптимизаций, даже тех, которые вы считаете актуальными.
Это избыточно. Но это из-за вашего кода, а не из-за компилятора. Этот макрос будет расширяться до этого:
// x = strlen(file)
// y = MAX_FILE_NAME - 1
(((strlen(file)) < (MAX_FILE_NAME - 1)) ? (strlen(file)) : (MAX_FILE_NAME - 1))
Помните, что препроцессор — это, по сути, просто прославленная машина для копирования и вставки. Вы звоните strlen дважды. Попробуй это:
size_t file_len = strlen(file);
file_len = MIN(file_len, MAX_FILE_NAME - 1);
Удивительно, забыл этот факт, спасибо
Как вы думаете, имеет ли смысл компилятор обнаруживать такого рода неэффективность? Например, если указатель файла является константой, то его данные не изменятся, поэтому два вызова могут быть определены как избыточные.
@e.ad Я имею в виду, что это должен быть особый случай, встроенный в компилятор, что маловероятно. Компилятор понятия не имеет, что означает strlen. Это всего лишь код C для компилятора. Компилятор должен знать (чего он, скорее всего, не знает), что strlen вернет тот же результат для той же строки.
Имеет смысл. еще раз спасибо, приму ваш ответ, когда истечет срок
Компилятор может знать о стандартных библиотечных функциях. Компиляторы C в наши дни знают, например, о строках формата printf.
@Jason На самых популярных целях вы обнаружите, что компилятор знает о strlen и целом ряде других функций стандартной библиотеки. Распространенная проблема, с которой сталкиваются люди при написании собственной реализации стандартной библиотеки (без соответствующих флагов компилятора), заключается в том, что компилятор распознает код, который выполняет strlen, memcpy и т. д., и заменяет их вызовом этих функций, поэтому в этих случаях вы заканчиваете вверх с бесконечной рекурсией. :)
Так что в этом случае оптимизирующий компилятор поймет, что ему может не понадобиться находить истинную длину строки?
@WeatherVane Возможно, но... не стал бы int F(char *file){ отбрасывать квалификатор. Я просто попробовал это из любопытства, и gcc не выдал никаких предупреждений.
@Siguza: 1) не могли бы вы сослаться на «компилятор распознает КОД, который делает strlen и т. д.», 2) я не до конца довожу ваш пример: у меня есть реализация memcpy, которая заменена вызовом std: memcpy, и у меня есть вызов в моем коде mem_cpy, насколько я вижу, он заканчивается std:memcpy, не так ли? Спасибо
@Jason Джейсон, дважды вычисляющий длину строки, - это одна из проблем, которую оптимизирующий компилятор может понять, что в любом случае это не нужно. Другая проблема заключается в том, будет ли компилятор кодировать функцию длины строки, которая останавливается до достижения терминатора 0.
@e.ad Siguza имеет в виду strlen стандартную библиотечную функцию. Компилятор может иметь дополнительные знания об этих функциях, так как он может предположить, что они будут существовать (при компоновке с libc). Я полагаю, он говорит, что это не сработает, например, для функции, которую я пишу, под названием string_length. Это просто функция, которую должен обработать компилятор.
@WeatherVane Извините, я не понимаю: «Другая проблема заключается в том, будет ли компилятор кодировать функцию длины строки, которая останавливается до достижения терминатора 0». Э: Вы говорите о дополнительном шаге помимо оптимизации второго вызова strlen?
@Jason Наверняка оптимизирующий компилятор заметит, что ему не нужно дважды вызывать strlen. Я спрашиваю, будет ли оптимизирующий компилятор использовать свою собственную версию strlen, которая также останавливается на MAX_FILE_NAME - 1 до достижения терминатора строки. Если строка длиннее, нет необходимости искать ее терминатор. Компиляторам разрешено кодировать любым законным способом, чтобы результат был таким же, как и при неоптимизации.
@e.ad вот: godbolt.org/z/vvdd65b4n
@e.ad - Компилятор понятия не имеет, что означает strlen. неправда; GCC и clang рассматривают его как встроенную функцию, если только вы не используете -fno-builtin-strlen. Они будут распространять через него константы для строковых литералов, а с включенной оптимизацией действительно знают, что это чистая функция (без побочных эффектов и зависит только от ее ввода) и будут выполнять удаление подвыражений констант при нескольких вызовах с одним и тем же аргументом, если они может доказать, что указанная строка не была записана между вызовами. godbolt.org/z/oh6d99M9Y показывает оба эффекта. Даже в -O0 strlen("abcd") становится 4
@e.ad: GCC и clang также в некоторых случаях будут встраивать некоторые функции, например. Раньше GCC встраивал циклы strlen вместо вызова реализации стандартной библиотеки, но такие случаи, как Почему этот код использует strlen в 6,5 раз медленнее с включенной оптимизацией GCC? показал, что это плохая идея для x86, особенно с -O1 стратегией использования repnz scasb, которая очень медленна для больших строк по сравнению с циклом SIMD.
@Siguza, у тебя здесь инфинитивная петля,
@0___________ в моем коде (слева) нет бесконечного цикла. Вот и все, что я хотел продемонстрировать.
@Siguza у вас есть ярлык strlen и jmp для strlen
@0___________ Вот что он демонстрировал.
@Siguza: Компилятор jmp strlen должен быть хвостовым вызовом библиотечной функции, но вы назвали свою функцию C strlen, так что она фактически переходит к самой себе. Назовите функцию C как-нибудь иначе; компиляция определений/реализаций стандартных функций C проблематична без -fno-builtin, поскольку GCC захочет заменить их вызовом этой стандартной функции.
@PeterCordes Я хорошо это знаю, это то, что я хотел продемонстрировать. Смотрите мой первый комментарий.
Есть ли способ оправдать поведение компилятора в этом случае? Мне интересно знать.
Компилятор перестраховывается.
При более высоких уровнях оптимизации компилятор использует внутреннее знание strlen() и «знает», что strlen(file) вернет то же значение при втором вызове.
Учитывать:
int file_len = MIN(rand(), MAX_FILE_NAME - 1);
MIN() может не возвращать минимум даже при включенной оптимизации, поскольку он должен вызвать rand() во второй раз, если первый раз было меньше.
Учитывать:
int file_len = MIN(some_user_funciton(file), MAX_FILE_NAME - 1);
Компилятор, вероятно, мало что знает о some_user_funciton(file) и поэтому вызывает some_user_funciton(file) второй раз, когда это необходимо.
Классный пример, спасибо. Может ли компилятор определить, какие функции являются детерминированными? или это как список поддерживаемых? Я имею в виду, например, представьте, что мы заменяем rand() на time()
@"Хорошие" компиляторы знают о функциях стандартной библиотеки. Некоторые могут даже анализировать локальные функции и оценивать их детерминированность. Кодируйте для переносимости и избегайте передачи аргументов функций в макросы, такие как MIN(). Поскольку ваш хороший код тестируется на другом сайте, лучше избегать того, чтобы какой-нибудь кодер ругал предшествующего автора.
@e.ad: strlen является встроенным по умолчанию, GCC и clang могут заменить strlen("abcd") константой 4. Для других менее широко используемых функций, о которых внутренние компоненты компилятора не имеют специальных знаний, объявление заголовка библиотеки может использовать __attribute__((const)) объявить, что это чистая функция, которая считывает только свои аргументы, а не какое-либо глобальное состояние. например большинство математических функций таковы. Или __attribute__((pure)), чтобы объявить, что он не имеет побочных эффектов (но может читать глобальные переменные). gcc.gnu.org/onlinedocs/gcc/…
Вы спрашиваете, почему оптимизированный код компилятора не заменил свою собственную версию strlen, которая останавливается на MAX_FILE_NAME - 1, прежде чем встретится с терминатором 0?