Nano Hash - криптовалюты, майнинг, программирование

Сколько стека и кучи (в байтах) требуется функции C в X86

Реализация функции sum в «C» выглядит следующим образом:

int sum(int b[], int c)
{
    int s,i;

    if (c<0)
    {
            printf("ERROR\n");
    }
    s = 0;

    for(i=0; i<c; ++i)
    {
        s = s + b[i];
    }
    return s;
}

Я хотел бы знать, сколько стека и кучи в байтах требуется для суммы функций на платформе X86 Linux? Как это узнать?

Будет ли вызов функции из обработчика прерывания проблематичным или успешным?


  • printf не может запускаться из обработчика прерывания; вы можете быть уже в середине printf, когда срабатывает прерывание. Помимо этого, памяти кучи, очевидно, нет, потому что нет динамического распределения. Использование стека зависит от компилятора, но должно быть около нуля за пределами обратного адреса возврата; все легко помещается в реестры. Скомпилируйте его и посмотрите на вывод компилятора, чтобы увидеть, что он делает, например. в обозревателе компиляторов: godbolt.org 02.01.2019
  • geeksforgeeks.org/memory-layout-of-c-program стоит прочитать 02.01.2019
  • @duong_dajgja: это несколько полезно, но они компилируются с отключенной оптимизацией, поэтому ничего не будет оптимизировано. В отличие от здесь, где -O0 версия этой функции будет использовать пространство стека для локальных пользователей. 02.01.2019
  • вы пометили это как драйвер устройства Linux. Вы запускаете это из обработчика прерываний? printf в этом контексте плохо. printk и trace_printk могут быть альтернативами, на которые вы хотели бы обратить внимание, если это работает в контексте ядра. 02.01.2019

Ответы:


1

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

Первый вопрос ОП:

Я хотел бы знать, сколько стека и кучи в байтах требуется для суммы функций на платформе X86 Linux? Как это узнать?

Мы можем разбить этот первый вопрос на 2 части. Один касается размера стека, а другой - размера кучи.

Размер стека:

Чтобы узнать, сколько стека использует ваша функция, вы можете использовать одну из диагностических прагм GCC, то есть прагму -Wframe-larger-than=<X>. Вот пример того, как его использовать. Сначала мы добавляем прагму в код и сохраняем файл.

main.cpp

#include <stdio.h>
#pragma GCC diagnostic error "-Wframe-larger-than=1"

int sum(int b[], int c) {
    int s,i;
    if (c<0) {
            printf("ERROR\n");
    }
    s = 0;
    for(i=0; i<c; ++i) {
        s = s + b[i];
    }
    return s;
}

Теперь мы можем попытаться скомпилировать код:

junglefox@ubuntu:~$ gcc -c main.cpp
main.cpp: In function ‘int sum(int*, int)’:
main.cpp:20:1: error: the frame size of 32 bytes is larger than 1 bytes [-Werror=frame-larger-than=]
 }
 ^
cc1plus: some warnings being treated as errors
junglefox@ubuntu:~$

который сообщает размер 32 байта.

  • Альтернативным методом измерения размера стека является использование флага компилятора stack-usage в GCC. Итак, мы удаляем или комментируем строку // #pragma GCC diagnostic error "-Wframe-larger-than=1" и пытаемся снова скомпилировать файл, как показано ниже.

junglefox@ubuntu:~$ gcc -c main.cpp -fstack-usage

Это создаст файл main.su.

junglefox@ubuntu:~$ cat main.su 
main.cpp:5:5:int sum(int*, int) 48  static

что показывает, что мы используем 48 байт стека.


Размер кучи

Чтобы узнать, какой размер кучи использует наша программа, мы воспользуемся инструментом valgrind Massif. Для этого нам сначала нужно добавить в наш код функцию main() (без которой мы не можем создать двоичный файл. А двоичный файл — это то, что нам нужно будет запускать с помощью valgrind). Итак, main.cpp теперь выглядит так,

#include <stdio.h>
// #pragma GCC diagnostic error "-Wframe-larger-than=1"

int sum(int b[], int c) {
    int s,i;
    if (c<0) {
            printf("ERROR\n");
    }
    s = 0;
    for(i=0; i<c; ++i) {
        s = s + b[i];
    }
    return s;
}

int main() {
    // As Peter pointed, uncomment one of the following lines,
    // for it to be a valid test. Also, compiler optimizations,
    // when turned on, can give different results.
    // sum(NULL,0);
    // sum(NULL,-1);
    return 0;
}

А теперь скомпилируем, соберем и запустим бинарник с помощью valgrind, как показано здесь:

junglefox@ubuntu:~$ gcc -o main main.cpp
junglefox@ubuntu:~$ valgrind ./main --tool=massif

Это сгенерирует кучу информации, которая выглядит примерно так:

    ==8179== Memcheck, a memory error detector
==8179== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==8179== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==8179== Command: ./main --tool=massif
==8179== 
==8179== 
==8179== HEAP SUMMARY:
==8179==     in use at exit: 0 bytes in 0 blocks
==8179==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==8179== 
==8179== All heap blocks were freed -- no leaks are possible
==8179== 
==8179== For counts of detected and suppressed errors, rerun with: -v
==8179== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

который сообщает об общем использовании кучи 0 килобайт.

Кроме того, как пытался объяснить @mevets, вы всегда можете заглянуть в базовый код сборки, сгенерированный компилятором. В GCC вы могли бы сделать,

junglefox@ubuntu:~/gcc -S main.cpp
junglefox@ubuntu:~/cat main.s

который покажет вам, как выглядит ваша функция в базовом выводе сборки.

ПРИМЕЧАНИЕ/РЕДАКТИРОВАТЬ: Но чтобы быть полным, в C или C++, без динамического выделения памяти с использованием malloc() или new, вы, как программист, НЕ используете кучу. Кроме того, если вы не объявляете массив внутри своей функции, вы не используете какой-либо значительный объем стека.


Второй вопрос ОП:

Будет ли вызов функции из обработчика прерывания проблематичным или успешным?

Как многие люди любезно указали в комментариях, НЕ используйте printf() в своем обработчике прерываний.

Цитата из этой ссылки:

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

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

Итак, помимо printf(), одна вещь, которая может занять много времени, это размер массива, который вы передаете этой функции, когда используется как Interrupt Service Routine. Он имеет сложность O(n). Если c слишком велико, ваша программа будет остановлена ​​на относительно долгое время, пока ISR не завершит выполнение этого цикла for().

03.01.2019
  • Хороший трюк для использования стека. Вы забыли включить оптимизацию, которая должна уменьшить ее почти до 0 для этой функции, в зависимости от того, что компилятор делает с printf. Но ваша программа тестирования использования кучи никогда не вызывает функцию, которую вы хотите протестировать. В этом случае, чтобы он вообще мог использовать какую-либо кучу, очевидно, мы должны передать ему неверный ввод, чтобы он вызывал printf. Но вам нужно вызвать его как часть выполнения программы, чтобы он был действительным тестом. 03.01.2019
  • Другие способы использования стека: глубоко вложенные вызовы функций, особенно через функции со средними и большими struct локальными переменными. Вот почему код файловой системы Linux XFS часто является виновником проблем со стеком ядра. Очевидно, что рекурсия, которая не оптимизирована, может использовать огромное количество стека, поэтому не делайте этого. 03.01.2019
  • @PeterCordes, ах да, конечно. Рекурсия. Спасибо, что указали на это. Это не приходило мне в голову, когда я писал это. Да, большие структуры с большим количеством переменных действительно также съедают «значительные» объемы стека. Спасибо, что подняли это тоже. 03.01.2019
  • @PeterCordes, странно! Я скомпилировал и снова запустил код с помощью valgrind после добавления строки sum(NULL, 0); в функцию main(). На этот раз о выделении кучи сообщается как 0. На этот раз я тоже отключил оптимизацию. Я удалил строку, снова скомпилировал и снова запустил с valgrind, и я снова вижу использование кучи как 0. Либо я видел вещи раньше, либо компилятор творит здесь какое-то волшебство, поскольку ранее он сообщал об использовании кучи ~ 84 КБ. В качестве последнего теста я использовал эту строку sum(NULL, -1); в функции main(), и теперь использование кучи точно равно 1 КБ - из-за printf() 03.01.2019
  • Я ожидаю ненулевое использование кучи от функций инициализации glibc, по крайней мере, на пике. Неудивительно, что большая часть его освобождается перед выходом. Даже если вы не включаете stdio.h или код ссылки, содержащий ссылку на printf, glibc все равно инициализирует некоторые таблицы и буферы. 03.01.2019
  • Отличное объяснение. Что касается использования стека, я заметил два разных результата в двух разных методах, то есть «-Wframe-larger-than=‹X› pragma» и «-fstack-usage», в чем может быть причина? Таким образом, время выполнения функции sum (buffer,n) в худшем случае равно O(n), верно? Как ведет себя система, если n равно UINTMAX_MAX и эта функция вызывается из обработчика прерывания, я имею в виду, что здесь я хотел бы знать, существует ли какое-либо пороговое ограничение по времени для завершения обработчика прерывания? Наконец, какие еще параметры платформы x86 влияют на время выполнения этой «функции sum()»? 04.01.2019
  • @MuniSekhar, почему два метода стека дают два разных результата, я не знаю. Это может быть что-то *за кулисами, выполняемое компилятором. Тем не менее 32bytes и 48bytes «довольно близки». Я не знаю никаких ограничений по времени, налагаемых на обслуживание прерываний. Что произойдет, так это то, что этот ISR заблокирует все остальное. Тогда вы можете потерять входящие данные, отправляемые другим оборудованием. Подробнее об этом здесь. Системы реального времени — это отдельная история. По поводу параметров X86 лучше задать отдельный вопрос. 04.01.2019

  • 2

    Сколько стека? После удаления printf требуется 0 байт стека; функция достаточно проста для работы с 3-мя регистрами:

    .globl _sum
    _sum: /* (int *b, int c); */
        mov 4(%esp), %edx
        mov 8(%esp), %ecx
        cmp $0, %ecx
        jl   badcount
        leal (%edx,%ecx,4), %ecx
        xor %eax, %eax
    nxt:
        cmp %ecx, %edx
        je    done
        add (%edx), %eax
        add $4, %edx
        jmp nxt
    done:
        ret
    badcount:
        mov $-1, %eax
        ret
    

    Вы должны иметь возможность полагаться на компилятор, чтобы он не делал совершенно глупых вещей (независимо от того, что думает стандартный С##).

    Если вы так далеко зашли в угол, что вам нужно считать байты стека, поищите ошибку ранее на своем пути. Даже Франк и Фертер понимали, что устранение симптомов отличается от устранения причины.

    02.01.2019

    3

    C не имеет понятия "стек"; и когда код, написанный на C, компилируется, вы должны думать о нем как о искаженном до неузнаваемой форме.

    Например, если вызывающий делает это:

        myArray[2] = 99;
        result = sum( myArray, 4);
    

    Затем компилятор может встроить полностью отдельную копию функции sum(), а затем (используя такие оптимизации, как сворачивание констант, устранение мертвого кода и разворачивание цикла) преобразовать эту отдельную копию функции в (эквивалент):

        result = myArray[0] + myArray[1] + 99 + myArray[3];
    

    .. а затем аннотировать его (или преобразовать в форму «единого статического назначения»), чтобы обеспечить больший параллелизм, например:

        temp1 = (myArray[0] + myArray[1]); temp2 = (99 + myArray[3]);
        result = temp1 + temp2;
    

    ..и затем преобразовать во что-то вроде:

        mov eax,[myArray]
        mov ebx,[myArray+4]
        lea eax,[eax+ebx]
    
        mov ecx,99
        mov edx,[myArray+12]
        lea ecx,[ecx+edx]
    
        lea eax,[eax+ecx]
    
        mov [result],eax
    

    ..а затем оптимизировать и изменить порядок инструкций, чтобы получить:

        mov eax,[myArray]
        mov ebx,[myArray+4]
        mov edx,[myArray+12]
        lea eax,[eax+ebx]
        lea eax,[eax+edx+99]
        mov [result],eax
    

    Обратите внимание, что это не похоже на исходный код - например. нет printf(), нет петли и нет ветвей.

    Конечно (учитывая, что функция не static, и при условии, что не выполняется оптимизация времени компоновки или генерация кода времени компоновки), компилятор, вероятно, также сгенерирует версию функции "Я ничего не знаю о вызывающей стороне" и засунет ее в выходной объектный файл, если он нужен компоновщику; и (если никакие другие объектные файлы не используют эту функцию) компоновщик может отбросить эту версию функции. В таком случае; вы можете посмотреть на код, сгенерированный компилятором для версии «Я ничего не знаю о вызывающей стороне», а затем сделать бесполезные/неправильные предположения, основанные на количестве стека, используемом кодом, который отбрасывается и никогда не выполняется.

    03.01.2019
    Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

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

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

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

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

    Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
    В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..