Неоптимизированное поведение компилятора руки

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

#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 * длины файла. что кажется ненужным. Есть ли способ оправдать поведение компилятора в этом случае? Мне интересно знать.

Вы спрашиваете, почему оптимизированный код компилятора не заменил свою собственную версию strlen, которая останавливается на MAX_FILE_NAME - 1, прежде чем встретится с терминатором 0?

Weather Vane 16.02.2023 20:10

Вы компилируете без оптимизации. Почему вы ожидаете оптимизированный код?

fuz 16.02.2023 20:56

Нужно ДВ. Какой будет результат MIN(x++)?

0___________ 16.02.2023 21:37

@fuz в моем реальном примере кода это происходит с O3. также можно подумать, что некоторые оптимизации актуальны даже для процесса компиляции по умолчанию.

e.ad 18.02.2023 19:56

@e.ad Без оптимизации компилятор может и будет генерировать настолько глупый код, насколько ему нравится. Не ждите никаких оптимизаций, даже тех, которые вы считаете актуальными.

fuz 18.02.2023 23:48
Ускорьте разработку веб-приложений Laravel с помощью этих бесплатных стартовых наборов
Ускорьте разработку веб-приложений Laravel с помощью этих бесплатных стартовых наборов
Laravel - это мощный PHP-фреймворк, используемый для создания масштабируемых и надежных веб-приложений. Одним из преимуществ Laravel является его...
Что такое двойные вопросительные знаки (??) в JavaScript?
Что такое двойные вопросительные знаки (??) в JavaScript?
Как безопасно обрабатывать неопределенные и нулевые значения в коде с помощью Nullish Coalescing
Создание ресурсов API Laravel: Советы по производительности и масштабируемости
Создание ресурсов API Laravel: Советы по производительности и масштабируемости
Создание API-ресурса Laravel может быть непростой задачей. Она требует глубокого понимания возможностей Laravel и лучших практик, чтобы обеспечить...
Как сделать компонент справочного центра с помощью TailwindCSS
Как сделать компонент справочного центра с помощью TailwindCSS
Справочный центр - это веб-сайт, где клиенты могут найти ответы на свои вопросы и решения своих проблем. Созданный для решения многих распространенных...
Асинхронная передача данных с помощью sendBeacon в JavaScript
Асинхронная передача данных с помощью sendBeacon в JavaScript
В современных веб-приложениях отправка данных из JavaScript на стороне клиента на сервер является распространенной задачей. Одним из популярных...
Как подобрать выигрышные акции с помощью анализа и визуализации на Python
Как подобрать выигрышные акции с помощью анализа и визуализации на Python
Отказ от ответственности: Эта статья предназначена только для демонстрации и не должна использоваться в качестве инвестиционного совета.
1
5
167
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Это избыточно. Но это из-за вашего кода, а не из-за компилятора. Этот макрос будет расширяться до этого:

// 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 16.02.2023 20:12

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

e.ad 16.02.2023 20:13

@e.ad Я имею в виду, что это должен быть особый случай, встроенный в компилятор, что маловероятно. Компилятор понятия не имеет, что означает strlen. Это всего лишь код C для компилятора. Компилятор должен знать (чего он, скорее всего, не знает), что strlen вернет тот же результат для той же строки.

Jason 16.02.2023 20:16

Имеет смысл. еще раз спасибо, приму ваш ответ, когда истечет срок

e.ad 16.02.2023 20:17

Компилятор может знать о стандартных библиотечных функциях. Компиляторы C в наши дни знают, например, о строках формата printf.

Erik Eidt 16.02.2023 20:20

@Jason На самых популярных целях вы обнаружите, что компилятор знает о strlen и целом ряде других функций стандартной библиотеки. Распространенная проблема, с которой сталкиваются люди при написании собственной реализации стандартной библиотеки (без соответствующих флагов компилятора), заключается в том, что компилятор распознает код, который выполняет strlen, memcpy и т. д., и заменяет их вызовом этих функций, поэтому в этих случаях вы заканчиваете вверх с бесконечной рекурсией. :)

Siguza 16.02.2023 20:20

Так что в этом случае оптимизирующий компилятор поймет, что ему может не понадобиться находить истинную длину строки?

Weather Vane 16.02.2023 20:23

@WeatherVane Возможно, но... не стал бы int F(char *file){ отбрасывать квалификатор. Я просто попробовал это из любопытства, и gcc не выдал никаких предупреждений.

Jason 16.02.2023 20:25

@Siguza: 1) не могли бы вы сослаться на «компилятор распознает КОД, который делает strlen и т. д.», 2) я не до конца довожу ваш пример: у меня есть реализация memcpy, которая заменена вызовом std: memcpy, и у меня есть вызов в моем коде mem_cpy, насколько я вижу, он заканчивается std:memcpy, не так ли? Спасибо

e.ad 16.02.2023 20:25

@Jason Джейсон, дважды вычисляющий длину строки, - это одна из проблем, которую оптимизирующий компилятор может понять, что в любом случае это не нужно. Другая проблема заключается в том, будет ли компилятор кодировать функцию длины строки, которая останавливается до достижения терминатора 0.

Weather Vane 16.02.2023 20:28

@e.ad Siguza имеет в виду strlen стандартную библиотечную функцию. Компилятор может иметь дополнительные знания об этих функциях, так как он может предположить, что они будут существовать (при компоновке с libc). Я полагаю, он говорит, что это не сработает, например, для функции, которую я пишу, под названием string_length. Это просто функция, которую должен обработать компилятор.

Jason 16.02.2023 20:28

@WeatherVane Извините, я не понимаю: «Другая проблема заключается в том, будет ли компилятор кодировать функцию длины строки, которая останавливается до достижения терминатора 0». Э: Вы говорите о дополнительном шаге помимо оптимизации второго вызова strlen?

Jason 16.02.2023 20:31

@Jason Наверняка оптимизирующий компилятор заметит, что ему не нужно дважды вызывать strlen. Я спрашиваю, будет ли оптимизирующий компилятор использовать свою собственную версию strlen, которая также останавливается на MAX_FILE_NAME - 1 до достижения терминатора строки. Если строка длиннее, нет необходимости искать ее терминатор. Компиляторам разрешено кодировать любым законным способом, чтобы результат был таким же, как и при неоптимизации.

Weather Vane 16.02.2023 20:37

@e.ad вот: godbolt.org/z/vvdd65b4n

Siguza 16.02.2023 20:44

@e.ad - Компилятор понятия не имеет, что означает strlen. неправда; GCC и clang рассматривают его как встроенную функцию, если только вы не используете -fno-builtin-strlen. Они будут распространять через него константы для строковых литералов, а с включенной оптимизацией действительно знают, что это чистая функция (без побочных эффектов и зависит только от ее ввода) и будут выполнять удаление подвыражений констант при нескольких вызовах с одним и тем же аргументом, если они может доказать, что указанная строка не была записана между вызовами. godbolt.org/z/oh6d99M9Y показывает оба эффекта. Даже в -O0 strlen("abcd") становится 4

Peter Cordes 16.02.2023 21:04

@e.ad: GCC и clang также в некоторых случаях будут встраивать некоторые функции, например. Раньше GCC встраивал циклы strlen вместо вызова реализации стандартной библиотеки, но такие случаи, как Почему этот код использует strlen в 6,5 раз медленнее с включенной оптимизацией GCC? показал, что это плохая идея для x86, особенно с -O1 стратегией использования repnz scasb, которая очень медленна для больших строк по сравнению с циклом SIMD.

Peter Cordes 16.02.2023 21:09

@Siguza, у тебя здесь инфинитивная петля,

0___________ 16.02.2023 21:38

@0___________ в моем коде (слева) нет бесконечного цикла. Вот и все, что я хотел продемонстрировать.

Siguza 16.02.2023 21:48

@Siguza у вас есть ярлык strlen и jmp для strlen

0___________ 16.02.2023 21:54

@0___________ Вот что он демонстрировал.

Jason 16.02.2023 22:15

@Siguza: Компилятор jmp strlen должен быть хвостовым вызовом библиотечной функции, но вы назвали свою функцию C strlen, так что она фактически переходит к самой себе. Назовите функцию C как-нибудь иначе; компиляция определений/реализаций стандартных функций C проблематична без -fno-builtin, поскольку GCC захочет заменить их вызовом этой стандартной функции.

Peter Cordes 16.02.2023 22:31

@PeterCordes Я хорошо это знаю, это то, что я хотел продемонстрировать. Смотрите мой первый комментарий.

Siguza 16.02.2023 22:36
Есть ли способ оправдать поведение компилятора в этом случае? Мне интересно знать.

Компилятор перестраховывается.

При более высоких уровнях оптимизации компилятор использует внутреннее знание 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()

e.ad 16.02.2023 21:00

@"Хорошие" компиляторы знают о функциях стандартной библиотеки. Некоторые могут даже анализировать локальные функции и оценивать их детерминированность. Кодируйте для переносимости и избегайте передачи аргументов функций в макросы, такие как MIN(). Поскольку ваш хороший код тестируется на другом сайте, лучше избегать того, чтобы какой-нибудь кодер ругал предшествующего автора.

chux - Reinstate Monica 16.02.2023 21:07

@e.ad: strlen является встроенным по умолчанию, GCC и clang могут заменить strlen("abcd") константой 4. Для других менее широко используемых функций, о которых внутренние компоненты компилятора не имеют специальных знаний, объявление заголовка библиотеки может использовать __attribute__((const)) объявить, что это чистая функция, которая считывает только свои аргументы, а не какое-либо глобальное состояние. например большинство математических функций таковы. Или __attribute__((pure)), чтобы объявить, что он не имеет побочных эффектов (но может читать глобальные переменные). gcc.gnu.org/onlinedocs/gcc/…

Peter Cordes 16.02.2023 21:12

Другие вопросы по теме