Является ли хорошей практикой использование меньших типов данных для переменных для экономии памяти?

32

Когда я впервые выучил язык C ++, я узнал, что, кроме int, float и т. Д., В этом языке существуют меньшие или большие версии этих типов данных. Например, я мог бы назвать переменную х

int x;
or 
short int x;

Основное отличие состоит в том, что short int занимает 2 байта памяти, тогда как int занимает 4 байта, а short int имеет меньшее значение, но мы могли бы также вызвать его, чтобы сделать его еще меньше:

int x;
short int x;
unsigned short int x;

что еще более ограничительно.

Мой вопрос здесь заключается в том, является ли хорошей практикой использование отдельных типов данных в зависимости от того, какие значения принимает ваша переменная в программе. Является ли хорошей идеей всегда объявлять переменные в соответствии с этими типами данных?

Bugster
источник
3
Вы знаете шаблон проектирования Flyweight ? «объект, который минимизирует использование памяти, разделяя как можно больше данных с другими подобными объектами; это способ использовать объекты в больших количествах, когда простое повторное представление будет использовать недопустимый объем памяти ...»
gnat
5
При стандартных настройках компилятора компоновки / выравнивания переменные будут в любом случае выровнены по 4-байтовым границам, так что их вообще не будет никакой разницы.
nikie
36
Классический случай преждевременной оптимизации.
шарфридж
1
@nikie - они могут быть выровнены по 4-байтовой границе на процессоре x86, но в общем случае это не так. MSP430 размещает символ на любом байтовом адресе, а все остальное на четном байтовом адресе. Я думаю, что AVR-32 и ARM Cortex-M одинаковы.
uɐɪ
3
Вторая часть вашего вопроса подразумевает, что добавление unsignedтак или иначе заставляет целое число занимать меньше места, что, конечно, неверно. Он будет иметь такое же количество дискретных представимых значений (давать или брать 1 в зависимости от того, как представлен знак), но просто смещается исключительно в положительное значение.
underscore_d

Ответы:

41

В большинстве случаев стоимость места незначительна, и вам не следует об этом беспокоиться, однако вам следует беспокоиться о дополнительной информации, которую вы предоставляете, объявляя тип. Например, если вы:

unsigned int salary;

Вы даете полезную информацию другому разработчику: зарплата не может быть отрицательной.

Разница между short, int, long редко вызывает проблемы с пространством в вашем приложении. Вы, скорее всего, случайно сделаете ложное предположение, что число всегда будет соответствовать какому-либо типу данных. Вероятно, безопаснее всегда использовать int, если вы не уверены на 100%, что ваши цифры всегда будут очень маленькими. Даже тогда, это вряд ли сэкономит вам сколько-нибудь заметное количество места.

Oleksi
источник
5
Это правда, что в наши дни редко возникают проблемы, но если вы разрабатываете библиотеку или класс, который будет использовать другой разработчик, то это другой вопрос. Возможно, им потребуется хранилище для миллиона таких объектов, и в этом случае разница будет большой - 4 МБ по сравнению с 2 МБ только для одного этого поля.
dodgy_coder
30
Использование unsignedв этом случае плохая идея: не только зарплата не может быть отрицательной, но и разница между двумя зарплатами также не может быть отрицательной. (В общем, использование unsigned для чего-либо, кроме разбивки битов, и определение поведения при переполнении - плохая идея.)
zvrba
16
@zvrba: Разница между двумя зарплатами сама по себе не является заработной платой, поэтому допустимо использовать другой тип с подписью.
JeremyP
12
@JeremyP Да, но если вы используете C (и, похоже, это верно и в C ++), вычитание целого числа без знака приводит к целому числу без знака , которое не может быть отрицательным. Он может превратиться в правильное значение, если вы приведете его к целому числу со знаком, но результатом вычисления будет целое число без знака. Смотрите также этот ответ для большей странности в вычислениях со знаком и без знака - вот почему вы никогда не должны использовать переменные без знака, если только вы не перебираете биты.
Такро
5
@zvrba: разница в денежном выражении, а не в зарплате. Теперь вы можете утверждать, что зарплата - это также денежная величина (ограниченная положительными числами и 0 путем проверки входных данных, что и делает большинство людей), но разница между двумя зарплатами сама по себе не является зарплатой.
JeremyP
29

ОП ничего не сказал о типе системы, для которой они пишут программы, но я предполагаю, что ОП думала о типичном ПК с ГБ памяти, поскольку упоминается C ++. Как говорится в одном из комментариев, даже с таким типом памяти, если у вас есть несколько миллионов элементов одного типа - например, массива - тогда размер переменной может иметь значение.

Если вы попадаете в мир встраиваемых систем - который на самом деле не выходит за рамки вопроса, поскольку OP не ограничивает его ПК - тогда размер типов данных очень важен. Я только что закончил быстрый проект на 8-битном микроконтроллере, который имеет только 8 тыс. Слов памяти программ и 368 байт оперативной памяти. Там, очевидно, каждый байт имеет значение. Никто никогда не использует переменную больше, чем им нужно (как с точки зрения пространства, так и размера кода - 8-битные процессоры используют много инструкций для манипулирования 16- и 32-битными данными). Зачем использовать процессор с такими ограниченными ресурсами? В больших количествах они могут стоить всего четверть.

В настоящее время я работаю над другим встраиваемым проектом с 32-разрядным микроконтроллером на основе MIPS, который имеет 512 Кбайт флэш-памяти и 128 Кбайт оперативной памяти (и стоит около $ 6). Как и в случае с ПК, «естественный» размер данных составляет 32 бита. Теперь с точки зрения кода становится более эффективным использование целочисленных значений для большинства переменных вместо символов или кратких значений. Но еще раз, любой тип массива или структуры должен быть рассмотрен, гарантированы ли меньшие типы данных. В отличие от компиляторов для более крупных систем, более вероятно, что переменные в структуре будут упакованы во встроенную систему. Я стараюсь всегда ставить сначала все 32-битные переменные, затем 16-битные, а затем 8-битные, чтобы избежать каких-либо «дырок».

tcrosley
источник
10
+1 за то, что к встроенным системам применяются разные правила. Тот факт, что C ++ упоминается, не означает, что целью является ПК. Один из моих недавних проектов был написан на C ++ для процессора с 32 КБ ОЗУ и 256 КБ Flash.
uɐɪ
13

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

преимущества

  • Меньшие типы используют меньше памяти в большинстве систем.
  • Меньшие типы дают более быстрые вычисления на некоторых системах. Особенно верно для float против double во многих системах. А меньшие типы int также дают значительно более быстрый код на 8- или 16-битных процессорах.

Недостатки

  • Многие процессоры имеют требования к выравниванию. Некоторые обращаются к выровненным данным быстрее, чем к невыровненным. Некоторые должны выровнять данные, чтобы иметь к ним доступ. Целочисленные типы большего размера равны одной выровненной единице, поэтому они, скорее всего, не выровнены. Это означает, что компилятор может быть вынужден поместить ваши меньшие целые числа в большие. И если меньшие типы являются частью более крупной структуры, вы можете получить различные байты заполнения, незаметно вставленные компилятором в любом месте структуры, чтобы исправить выравнивание.
  • Опасные неявные преобразования. В C и C ++ есть несколько неясных, опасных правил того, как переменные повышаются до более крупных, неявно без преобразования типов. Существует два набора правил неявного преобразования, сплетенных друг с другом, называемых «правилами целочисленного продвижения» и «обычными арифметическими преобразованиями». Подробнее о них читайте здесь . Эти правила являются одной из наиболее распространенных причин ошибок в C и C ++. Вы можете избежать множества проблем, просто используя один и тот же целочисленный тип во всей программе.

Я советую вот так:

system                             int types

small/low level embedded system    stdint.h with smaller types
32-bit embedded system             stdint.h, stick to int32_t and uint32_t.
32-bit desktop system              Only use (unsigned) int and long long.
64-bit system                      Only use (unsigned) int and long long.

В качестве альтернативы вы можете использовать int_leastn_tили int_fastn_tиз stdint.h, где n - это число 8, 16, 32 или 64. int_leastn_tТип означает «Я хочу, чтобы это было не менее n байтов, но мне все равно, если компилятор выделяет его как больший тип, чтобы соответствовать выравниванию ".

int_fastn_t означает «я хочу, чтобы это было длиной n байтов, но если это заставит мой код работать быстрее, компилятор должен использовать больший тип, чем указано».

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


источник
Пятно о выравнивании. В моем текущем проекте безвозмездное использование uint8_t на 16-разрядном MSP430 привело к сбоям в работе MCU таинственным образом (скорее всего, неправильный доступ произошел где-то, возможно, ошибка GCC, а может и нет) - просто замена всего uint8_t на «unsigned» устранила сбои. Использование 8-битных типов на> 8-битных дугах, если не фатально, по крайней мере неэффективно: компилятор генерирует дополнительные инструкции 'и reg, 0xff'. Используйте 'int / unsigned' для переносимости и освободите компилятор от дополнительных ограничений.
Алексей
11

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

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

Как это поможет вам? Что ж, вам решать, для какой ситуации вы кодируете. Почти для каждой программы, которую я когда-либо писал, достаточно просто довериться компилятору, чтобы оптимизировать вещи и использовать наиболее удобный для вас тип данных. Если вам нужна высокая точность, используйте большие типы данных с плавающей точкой. Если вы работаете только с положительными значениями, вы, вероятно, можете использовать целое число без знака, но по большей части достаточно просто использовать тип данных int.

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

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

S.Robins
источник
6

Как прокомментировал шарфридж , это

Классический случай преждевременной оптимизации .

Попытка оптимизации использования памяти может повлиять на другие области производительности, и золотые правила оптимизации :

Первое правило оптимизации программы: не делайте этого .

Второе правило оптимизации программы (только для экспертов!): Пока не делайте этого ».

- Майкл А. Джексон

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

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

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

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

По этой причине современные компиляторы игнорируют ваши предложения. Как ники комментарии:

При стандартных настройках компилятора компоновки / выравнивания переменные будут в любом случае выровнены по 4-байтовым границам, так что их вообще не будет никакой разницы.

Второй угадать ваш компилятор на свой страх и риск.

Есть место для такой оптимизации при работе с терабайтными наборами данных или встроенными микроконтроллерами, но для большинства из нас это не представляет особой проблемы.

Марк Бут
источник
3

Основное отличие состоит в том, что short int занимает 2 байта памяти, тогда как int занимает 4 байта, а short int имеет меньшее значение, но мы могли бы также вызвать его, чтобы сделать его еще меньше:

Это неверно Вы не можете делать предположения о том, сколько байтов содержит каждый тип, кроме charодного байта и не менее 8 бит на байт, при этом размер каждого типа больше или равен предыдущему.

Преимущества производительности невероятно малы для переменных стека - они, скорее всего, будут выровнены / дополнены в любом случае.

Из-за этого shortи longпрактически не используются в наше время, и вы почти всегда лучше использовать int.


Конечно, есть и то, stdint.hчто идеально подходит для использования, когда intего не режут. Если вы когда-либо выделяете огромные массивы целых чисел / структур, это intX_tимеет смысл, так как вы можете быть эффективными и полагаться на размер шрифта. Это совсем не преждевременно, так как вы можете сэкономить мегабайты памяти.

Pubby
источник
1
На самом деле, с появлением 64-битных сред, longможет отличаться от int. Если ваш компилятор LP64, int32-битный и long64- intбитный, и вы обнаружите, что s все еще может быть выровнен на 4 байта (например, мой компилятор).
JeremyP
1
@ JeremyP Да, я сказал иначе или что-то?
Pubby
Ваше последнее предложение, которое претендует на короткое и длинное, практически бесполезно. Лонг, безусловно, имеет применение, хотя бы в качестве базового типаint64_t
JeremyP
@JeremyP: Вы можете просто жить с int и long long.
gnasher729
@ gnasher729: Что вы используете, если вам нужна переменная, которая может содержать значения более 65 тысяч, но не более миллиарда? int32_t, int_fast32_tИ longвсе хорошие варианты, long longпросто расточительно, и intнепереносимой.
Бен Фойгт
3

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

Хорошая идея - использовать разные типы данных для разных видов информации в вашем приложении. Однако, вероятно, НЕ рекомендуется использовать встроенные типы для этого, если у вас нет серьезных проблем с производительностью (которые были измерены и проверены и т. Д.).

Если мы хотим смоделировать температуру в Кельвинах в нашем приложении, мы МОЖЕМ использовать ushortили uintили что-то подобное, чтобы обозначить, что «понятие отрицательных степеней Кельвина абсурдно и ошибка логики предметной области». Идея, стоящая за этим, звучит здраво, но вы не идете до конца. Мы поняли, что у нас не может быть отрицательных значений, поэтому удобно, если мы сможем заставить компилятор убедиться, что никто не присваивает отрицательное значение температуре Кельвина. ТАКЖЕ верно, что вы не можете делать побитовые операции при температурах. И вы не можете добавить меру веса (кг) к температуре (K). Но если вы смоделируете температуру и массу как uints, мы можем сделать это.

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

Решение состоит в том, чтобы определить новые типы, которые инкапсулируют инварианты. Таким образом, вы можете убедиться, что деньги - это деньги, а расстояния - это расстояния, и вы не можете сложить их вместе, и вы не можете создать отрицательное расстояние, но вы МОЖЕТЕ создать отрицательное количество денег (или долг). Конечно, эти типы будут использовать встроенные типы внутри, но это скрыто от клиентов. Относительно вашего вопроса о производительности / потреблении памяти, такого рода вещи могут позволить вам изменить внутреннее хранение вещей, не меняя интерфейс ваших функций, которые работают на ваших доменных объектах, если вы обнаружите, что чертовски shortмало большой.

Сара
источник
1

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

uint8_least_t(хранение) -> uint8_fast_t(обработка) -> uint8_least_t(хранение).

Например, вы берете 8- sourceбитный символ , 16-битные коды dictionariesи некоторые 32-битные constants. Чем вы обрабатываете 10-15 битные операции с ними и выводите 8 битные destination.

Давайте представим, что вам нужно обработать 2 гигабайта source. Количество битовых операций огромно. Вы получите отличный бонус производительности, если во время обработки переключитесь на быстрые типы. Быстрые типы могут быть разными для каждого семейства процессоров. Вы можете включать stdint.hи использовать uint_fast8_t, uint_fast16_t, uint_fast32_tи т.д.

Вы можете использовать uint_least8_tвместо uint8_tпереносимости. Но на самом деле никто не знает, какой современный процессор будет использовать эту функцию. VAC машина - музейный экспонат. Так что, возможно, это перебор.

puchu
источник
1
Несмотря на то, что вы можете иметь представление о перечисленных вами типах данных, вы должны объяснить, почему они лучше, а не просто заявить, что они лучше. Для таких людей, как я, которые не знакомы с этими типами данных, я должен был гуглить их, чтобы понять, о чем вы говорите.
Питер М.