Размер функции в памяти

Обновлено: 04.07.2024

Тем не менее, эта аналогия не совсем подходит к программированию, так как переменные могут занимать больше 1 байта памяти. Следовательно, одна переменная может использовать 2, 4 или даже 8 последовательных адресов. Объем памяти, который использует переменная, зависит от типа данных этой переменной. Так как мы, как правило, получаем доступ к памяти через имена переменных, а не через адреса памяти, то компилятор может скрывать от нас все детали работы с переменными разных размеров.

Есть несколько причин по которым полезно знать, сколько памяти занимает определенная переменная/тип данных.

Во-первых, чем больше она занимает, тем больше информации сможет хранить. Так как каждый бит содержит либо 0 , либо 1 , то 1 бит может иметь 2 возможных значения.

2 бита могут иметь 4 возможных значения:

бит 0 бит 1
0 0
0 1
1 0
1 1

3 бита могут иметь 8 возможных значений:

бит 0 бит 1 бит 2
0 0 0
0 0 1
0 1 0
0 1 1
1 0 0
1 0 1
1 1 0
1 1 1

По сути, переменная с n-ным количеством бит может иметь 2 n возможных значений. Поскольку байт состоит из 8 бит, то он может иметь 2 8 (256) возможных значений.

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

Размер основных типов данных в C++

Язык C++ гарантирует только их минимальный размер:

Тип Минимальный размер
Логический тип данных bool 1 байт
Символьный тип данных char 1 байт
wchar_t 1 байт
char16_t 2 байта
char32_t 4 байта
Целочисленный тип данных short 2 байта
int 2 байта
long 4 байта
long long 8 байт
Тип данных с плавающей запятой float 4 байта
double 8 байт
long double 8 байт

Фактический размер переменных может отличаться на разных компьютерах, поэтому для его определения используют оператор sizeof.

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


На протяжении своей карьеры я слышал множество аргументов о длине функции. Более глубокий вопрос — когда код нужно выносить в отдельную функцию? Иногда рекомендации основаны на размере, например, функция должна помещаться на экране. Другие основаны на повторном использовании — любой код, используемый больше одного раза, должен быть вынесен в отдельную функцию. Но если код используется лишь один раз, то можно его оставить на месте. Мне кажется, что большим смыслом обладает аргумент о разделении намерения и реализации. Если нужно потратить время на поиски фрагмента кода чтобы понять, что он делает, то нужно вынести его в функцию и дать ей такое имя, которое отвечает на вопрос "что". Тогда в следующий раз смысл функции сразу будет очевидным, и в большинстве случаев вас не будет волновать то, как функция выполняет свою работу. Иными словами — что происходит в теле функции.

Когда я стал применять такой принцип, я развил в себе привычку писать очень маленькие функции — обычно не больше нескольких строк. Любая функция длиннее шести строк уже попахивает. Вполне обычное дело для меня — иметь функцию с одной строчкой кода. Кент Бек показал мне когда-то пример из оригинальной системы Smalltalk, и это помогло мне по-настоящему понять, что размер — это не важно. Smalltalk в те годы работал на черно-белых машинах. Если нужно было подсветить текст или графику, то приходилось реверсировать видео. Класс в Smalltalk, отвечающий за графику, содержал метод 'highlight', и в его реализации была лишь одна строка — вызов метода 'reverse'. Название метода было длиннее реализации, но это не имело значения, потому что между намерением и реализацией этого кода — большое расстояние.

Некоторые люди волнуются по поводу коротких функций, потому что их заботит влияние вызовов на производительность. Когда я был молод, это иногда имело значение, но сегодня это редкость. Оптимизирующие компиляторы часто работают лучше с короткими функциями, потому что их легче кэшировать. Как обычно, в оптимизации производительности имеют смысл в первую очередь рекомендации общего характера. Иногда правильное решение это вернуть код из функции обратно в прежнее место. Но зачастую наличие маленьких функций позволяет найти другие способы оптимизации. Я помню, когда люди были против наличия метода isEmpty для списков. Стандартным способом было aList.length == 0. Но здесь как раз тот случай, когда название функции указывает на намерение, и это может помочь с производительностью если существует более быстрый способ определения пустоты коллекции, нежели проверкой длины.

Маленькие функции вроде этой работают только если названия достаточно хороши, так что нужно уделять внимание этому. Со временем ваш навык будет улучшаться, и такой подход может сильно повышать само-документированность кода. Функции более высокого уровня могут читаться как история, и читатель выбирает, в какие функции углубиться если нужно узнать подробности.

метод в настоящее время у меня в голове следующее:
1. Получить указатель на функцию
2. Увеличьте указатель (&counter), пока я не достигну значения машинного кода для ret
3. Счетчик будет размером функции?

Edit1: чтобы уточнить, что я имею в виду под "размером", я имею в виду количество байтов (машинный код), которые составляют функцию.
Edit2: было несколько комментариев, спрашивающих, почему или что я планирую сделать с этим. Честный ответ: у меня нет намерения, и я не вижу преимуществ знания длины функций перед компиляцией. (хотя я уверен, что есть некоторые)

Это кажется мне допустимым методом, будет ли это работать?

нет, это не сработает:

  1. нет никакой гарантии, что ваша функция содержит только один ret инструкция.
  2. даже если он содержит только один ret , вы не можете просто посмотреть на отдельные байты - потому, что соответствующее значение может выглядеть как просто значение, а не инструкция.

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

можно получить все блоки функции, но это неестественный вопрос, чтобы спросить, Что такое "размер" функции. Оптимизированный код будет переставлять блоки кода в порядке выполнения и перемещать редко используемые блоки (пути исключений) во внешние части модуля. Дополнительные сведения см. В разделе Профили Оптимизаций например, как Visual C++ достигает этого в генерации кода времени связи. Таким образом, функция может начинаться с адреса 0x00001000, ветвь на 0x00001100 в перейти на 0x20001000 и ret и иметь некоторый код обработки исключений 0x20001000. В 0x00001110 запускается другая функция. Каков "размер" вашей функции? Он охватывает от 0x00001000 до +0x20001000, но он "владеет" только несколькими блоками в этом промежутке. Поэтому ваш вопрос должен быть непрошеным.

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

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

это не сработает. что, если это прыжок, манекен?--1-->, а затем цель прыжка? Ваш код будет обманут.

В общем, это невозможно чтобы сделать это со 100% точностью, потому что вы должны предсказать все пути кода, что похоже на решение проблема останова. Вы можете получить "довольно хорошую" точность, если вы реализуете свой собственный дизассемблер, но никакое решение не будет почти таким простым, как Вы себе представляете.

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

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

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

для тех, кто говорит, что анализ потока не то же самое, что решение проблемы остановки:

рассмотрим, что происходит, когда у вас есть следующий код:

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

разве это не похоже на проблему остановки?

Вау, я использую функцию подсчета размера все время, и у нее много и много применений. Она надежна? Ни за что. Это стандартный c++? Ни за что. Но именно поэтому вам нужно проверить его в дизассемблере, чтобы убедиться, что он работает, каждый раз, когда вы выпускаете новую версию. Флаги компилятора могут испортить порядок.

Кажется, что он лучше работает со статическими функциями. Глобальная оптимизация может убить его.

P. S. Я ненавижу людей, спрашивающих, почему вы хотите сделать это, и это невозможно и т. д. Прекрати задавать эти вопросы, пожалуйста. Звучит глупо. Программистов часто просят делать нестандартные вещи, потому что новые продукты почти всегда раздвигают границы того, что доступно. Если они этого не делают, ваш продукт, вероятно, является переосмыслением того, что уже сделано. Скучно.

реальное решение этого-копаться в документации вашего компилятора. Компилятор ARM, который мы используем, можно сделать для создания дампа сборки (кода.dis), из которого довольно тривиально вычесть смещения между заданной меткой искореженной функции и следующей меткой искореженной функции.

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

обратите внимание, что я (работая во встроенном пространстве) предположил, что вы говорили о пост-компиляция-анализ. По-прежнему можно было бы изучить эти промежуточные файлы программно как часть сборки при условии, что:

  • целевая функция находится в другом объекте
  • система сборки была обучена зависимостям
  • вы точно знаете, что компилятор построит эти объектные файлы

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

этой can работа в очень ограниченных сценариях. Я использую его в части утилиты ввода кода, которую я написал. Я не помню, где я нашел информацию, но у меня есть следующее (C++ в VS2005):

а потом в какой-то другой функции у меня есть:

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

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

что значит "размер функция"?

Если вы имеете в виду указатель функции, то он всегда составляет всего 4 байта для 32-битных систем.

Если вы имеете в виду размер кода, чем вы должны просто разбирать сгенерированный код и найти точку входа и ближе ret звонок. Один из способов сделать это-прочитать регистр указателя инструкции в начале и в конце вашей функции.

Если вы хотите выяснить количество инструкций, вызванных в средний случай для вашей функции вы можете использовать профилировщики и делить количество ушедших инструкций на количество вызовов.

Я думаю, что он будет работать на программах windows, созданных с msvc, так как для ветвей "ret", похоже, всегда приходит в конце (даже если есть ветви, которые возвращаются рано, это делает jne, чтобы пойти в конец). Однако вам понадобится какая-то библиотека дизассемблера, чтобы вычислить текущую длину кода операции, поскольку они являются переменной длиной для x86. Если вы этого не сделаете, вы столкнетесь с ложными срабатываниями.

Я не удивлюсь, если есть случаи, когда это не ловит.

в стандартном C++ нет средств для получения размера или длины функции.
См. мой ответ здесь:можно ли загрузить функцию в некоторую выделенную память и запустить ее оттуда?

В общем, знание размера функции используется во встроенных системах при копировании исполняемого кода из источника только для чтения (или медленного запоминающего устройства, такого как последовательная вспышка) в ОЗУ. Настольные и другие операционные системы загружают функции в память, используя другие методы, такие как динамические или общие библиотеки.

просто установите PAGE_EXECUTE_READWRITE по адресу, где вы получили свою функцию. Затем прочитайте каждый байт. Когда вы получили байт "0xCC", это означает, что конец функции actual_reading_address - 1.

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

Оказывается, нетривиально выяснить, сколько памяти фактически потребляется. В этой статье я расскажу о тонкостях управления памятью объекта Python и покажу, как точно измерить потребляемую память.

В этой статье я остановлюсь исключительно на CPython - основной реализации языка программирования Python. Эксперименты и выводы здесь не относятся к другим реализациям Python, таким как IronPython, Jython и PyPy.

Также я запустил числа на 64-битном Python 2.7. В Python 3 числа иногда немного отличаются (особенно для строк, которые всегда являются Unicode), но концепции одинаковы.

Практическое исследование использования памяти Python

Во-первых, давайте немного разберемся и получим конкретное представление о фактическом использовании памяти объектами Python.

Встроенная функция sys.getsizeof()

Модуль sys стандартной библиотеки предоставляет функцию getsizeof(). Эта функция принимает объект (и необязательный параметр по умолчанию), вызывает метод sizeof() объекта и возвращает результат, поэтому вы также можете сделать ваши объекты инспектируемыми.

Измерение памяти объектов Python

Давайте начнем с некоторых числовых типов:

Интересно. Целое число занимает 24 байта.

Хм . float также занимает 24 байта.

Вот это да. 80 байтов! Это действительно заставляет задуматься о том, хотите ли вы представлять большое количество вещественных чисел как числа с плавающей запятой или десятичные дроби.

Давайте перейдем к строкам и коллекциям:

Хорошо. Пустая строка занимает 37 байтов, и каждый дополнительный символ добавляет еще один байт. Это многое говорит о компромиссе между сохранением нескольких коротких строк, когда вы будете платить 37 байтов за каждую, а не одну длинную строку, где вы платите только один раз.

Строки Unicode ведут себя аналогично, за исключением того, что служебные данные составляют 50 байтов, и каждый дополнительный символ добавляет 2 байта. Это стоит учитывать, если вы используете библиотеки, которые возвращают строки Unicode, но ваш текст может быть представлен в виде простых строк.

Кстати, в Python 3 строки всегда имеют Unicode, а служебные данные составляют 49 байт (они где-то сохранили байт). Объект байтов имеет служебную информацию только 33 байта. Если у вас есть программа, которая обрабатывает много коротких строк в памяти, и вы заботитесь о производительности, рассмотрите Python 3.

В чем дело? Пустой список занимает 72 байта, но каждый дополнительный int добавляет всего 8 байтов, где размер int составляет 24 байта. Список, который содержит длинную строку, занимает всего 80 байтов.

История повторяется для кортежей. Накладные расходы пустого кортежа составляют 56 байтов против 72 списка. Опять же, эта разница в 16 байтов на последовательность - это низко висящий плод, если у вас есть структура данных с большим количеством небольших неизменяемых последовательностей.

Наборы и словари якобы вообще не растут при добавлении элементов, но отмечают огромные накладные расходы.

Суть в том, что у объектов Python огромные фиксированные накладные расходы. Если ваша структура данных состоит из большого количества объектов коллекций, таких как строки, списки и словари, которые содержат небольшое количество элементов каждый, вы много платите.

Функция deep_getsizeof()

Теперь, когда я напугал вас до полусмерти и продемонстрировал, что sys.getsizeof() может только сказать вам, сколько памяти занимает примитивный объект, давайте посмотрим на более адекватное решение. Функция deep_getsizeof() рекурсивно выполняет детализацию и вычисляет фактическое использование памяти графом объектов Python.

У этой функции есть несколько интересных аспектов. Она учитывает объекты, на которые ссылаются несколько раз, и учитывает их только один раз, отслеживая идентификаторы объектов. Другая интересная особенность реализации заключается в том, что она в полной мере использует абстрактные базовые классы модуля коллекций. Это позволяет функции очень лаконично обрабатывать любую коллекцию, которая реализует базовые классы Mapping или Container, вместо непосредственного обращения к множеству типов коллекций, таких как: строка, Unicode, байты, список, кортеж, dict, frozendict, OrderedDict, set, frozenset и т.д.

Давайте посмотрим на это в действии:

Строка длиной 7 занимает 44 байта (37 служебных данных + 7 байтов для каждого символа).

Пустой список занимает 72 байта (только накладные расходы).

python deep_getsizeof ([x], set ()) 124

Список, содержащий строку x, занимает 124 байта (72 + 8 + 44).

Список, содержащий строку x 5 раз, занимает 156 байтов (72 + 5 * 8 + 44).

Последний пример показывает, что deep_getsizeof() подсчитывает ссылки на один и тот же объект (строку x) только один раз, но подсчитывается указатель каждой ссылки.

Баг или фича

Оказывается, что у CPython есть несколько хитростей, поэтому числа, которые вы получаете от deep_getsizeof(), не полностью отражают использование памяти программой Python.

Подсчет ссылок

Python управляет памятью, используя семантику подсчета ссылок. Когда на объект больше не ссылаются, его память освобождается. Но пока есть ссылка, объект не будет освобожден. Такие вещи, как циклические ссылки, могут вас сильно укусить.

Маленькие объекты

Целые числа

CPython хранит глобальный список всех целых чисел в диапазоне [-5, 256]. Эта стратегия оптимизации имеет смысл, потому что маленькие целые числа всплывают повсюду, и, учитывая, что каждое целое число занимает 24 байта, оно экономит много памяти для типичной программы.

Это также означает, что CPython предварительно выделяет 266 * 24 = 6384 байта для всех этих целых чисел, даже если вы не используете большинство из них. Вы можете проверить это с помощью функции id(), которая дает указатель на фактический объект. Если вы называете id(x) несколько для любого x в диапазоне [-5, 256], вы будете каждый раз получать один и тот же результат (для одного и того же целого числа). Но если вы попробуете это для целых чисел за пределами этого диапазона, каждый из них будет отличаться (новый объект создается на лету каждый раз).

Вот несколько примеров в этом диапазоне:

Вот несколько примеров за пределами диапазона:

Память Python против системной памяти

Теперь эта память 100X может быть бесполезно захвачена в вашей программе, никогда больше не использоваться и лишать систему возможности выделять ее другим программам. Ирония заключается в том, что если вы используете модуль обработки для запуска нескольких экземпляров вашей программы, вы строго ограничите количество экземпляров, которые вы можете запустить на данном компьютере.

Профилировщик памяти

Чтобы измерить и измерить фактическое использование памяти вашей программой, вы можете использовать модуль memory_profiler. Я немного поиграл с этим, и я не уверен, что доверяю результатам. Он очень прост в использовании. Вы декорируете функцию (может быть главной (0 функция)) с помощью декоратора @profiler, и когда программа завершает работу, профилировщик памяти выводит на стандартный вывод удобный отчет, который показывает общее количество и изменения в памяти для каждой строки. Вот пример программы, которую я запускал под профилировщиком:

Как вы можете видеть, занято 22,9 МБ дополнительной памяти. Причина, по которой память не увеличивается при добавлении целых чисел как внутри, так и вне диапазона [-5, 256], а также при добавлении строки, заключается в том, что во всех случаях используется один объект. Непонятно, почему первый цикл диапазона (100000) в строке 8 добавляет 4,2 МБ, в то время как второй цикл в строке 10 добавляет всего 0,4 МБ, а третий цикл в строке 12 добавляет 0,8 МБ. Наконец, при удалении списков a, b и c освобождается -0.6MB для a и c, но для b добавляется 0.2MB. Я не могу понять эти странные результаты.

Заключение

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

Тем не менее, эта аналогия не совсем подходит к программированию, так как переменные могут занимать больше 1 байта памяти. Следовательно, одна переменная может использовать 2, 4 или даже 8 последовательных адресов. Объем памяти, который использует переменная, зависит от типа данных этой переменной. Так как мы, как правило, получаем доступ к памяти через имена переменных, а не через адреса памяти, то компилятор может скрывать от нас все детали работы с переменными разных размеров.

Есть несколько причин по которым полезно знать, сколько памяти занимает определенная переменная/тип данных.

Во-первых, чем больше она занимает, тем больше информации сможет хранить. Так как каждый бит содержит либо 0 , либо 1 , то 1 бит может иметь 2 возможных значения.

2 бита могут иметь 4 возможных значения:

бит 0 бит 1
0 0
0 1
1 0
1 1

3 бита могут иметь 8 возможных значений:

бит 0 бит 1 бит 2
0 0 0
0 0 1
0 1 0
0 1 1
1 0 0
1 0 1
1 1 0
1 1 1

По сути, переменная с n-ным количеством бит может иметь 2 n возможных значений. Поскольку байт состоит из 8 бит, то он может иметь 2 8 (256) возможных значений.

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

Размер основных типов данных в C++

Язык C++ гарантирует только их минимальный размер:

Тип Минимальный размер
Логический тип данных bool 1 байт
Символьный тип данных char 1 байт
wchar_t 1 байт
char16_t 2 байта
char32_t 4 байта
Целочисленный тип данных short 2 байта
int 2 байта
long 4 байта
long long 8 байт
Тип данных с плавающей запятой float 4 байта
double 8 байт
long double 8 байт

Фактический размер переменных может отличаться на разных компьютерах, поэтому для его определения используют оператор sizeof.

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

Читайте также: