Что такое size_t и ptrdiff_t

Андрей Карпов
Статей: 396

Аннотация

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

Введение

Сразу заметим, что данные в статье определения и рекомендации относятся к наиболее распространненым на данный момент архитектурам (IA-32, Intel 64, IA-64), и могут быть неточны по отношению к экзотическим архитектурам.

Типы size_t и ptrdiff_t были созданы для того, чтобы осуществлять корректную адресную арифметику. Долгое время было принято считать, что размер int совпадает с размером машинного слова (разрядностью микропроцессора) и его можно использовать в качестве индексов, для хранения размеров объектов или указателей. Соответственно адресная арифметика также строилась с использованием типов int и unsigned. Тип int используется в большинстве обучающих материалов по программированию на Си и Си++ в телах циклов и в качестве индексов. Практически каноническим выглядит пример следующего вида:

for (int i = 0; i < n; i++)
  a[i] = 0;

С развитием микропроцессоров и ростом их разрядности стало нерационально дальнейшее увеличение размерностей типа int. Причин для этого много: экономия используемой памяти, максимальная совместимость и так далее. В результате появилось несколько моделей данных, описывающих соотношение размеров базовых типов языка Си/Си++. В таблице N1 приведены основные модели данных и перечислены наиболее популярные системы их использующие.

https://import.viva64.com/docx/blog/size_t_and_ptrdiff_t_ru/image1.png

Таблица N1. Модели данных (data models)

Как видно из таблицы, не так просто выбрать тип переменной для хранения указателя или размера объекта. Чтобы наиболее красиво решить эту проблему и появились типы size_t и ptrdiff_t. Они гарантированно могут использоваться для адресной арифметики. Теперь каноническим должен стать следующий код:

for (ptrdiff_t i = 0; i < n; i++)
  a[i] = 0;

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

Тип size_t

Тип size_t - базовый беззнаковый целочисленный тип языка Си/Си++. Является типом результата, возвращаемого оператором sizeof. Размер типа выбирается таким образом, чтобы в него можно было записать максимальный размер теоретически возможного массива любого типа. Например, на 32-битной системе size_t будет занимать 32-бита, на 64-битной - 64-бита. Другими словами в переменную типа size_t может быть безопасно помещен указатель. Исключение составляют указатели на функции классов, но это особый случай. Хотя в size_t можно помещать указатель, для этих целей лучше подходит другой беззнаковый целочисленный тип uintptr_t, само название которого отражает эту возможность. Типы size_t и uintptr_t являются синонимами. Тип size_t обычно применяется для счетчиков циклов, индексации массивов, хранения размеров, адресной арифметики.

Максимально допустимым значением типа size_t является константа SIZE_MAX.

Тип ptrdiff_t

Тип ptrdiff_t - базовый знаковый целочисленный тип языка Си/Си++. Размер типа выбирается таким образом, чтобы в него можно было записать максимальный размер теоретически возможного массива любого типа. На 32-битной системе ptrdiff_t будет занимать 32-бита, на 64-битной - 64-бита. Как и в size_t в переменную типа ptrdiff_t может быть безопасно помещен указатель, за исключением указателя на функцию класса. Также ptrdiff_t является типом результата выражения, где один указатель вычитается из другого (ptr1-ptr2). Тип ptrdiff_t обычно применяется для счетчиков циклов, индексации массивов, хранения размеров, адресной арифметики. У типа ptrdiff_t есть синоним intptr_t, название которого лучше отражает, что тип может хранить в себе указатель.

Переносимость size_t и ptrdiff_t

Типы size_t и ptrdiff_t позволяют писать переносимый код. Размер size_t и ptrdiff_t всегда совпадают с размером указателя. По этой причине именно эти типы следует использовать в качестве индексов больших массивов, для хранения указателей и арифметики с указателями.

Разработчики Linux приложений часто используют для этих целей тип long. В рамках 32-битных и 64-битных моделей данных, принятых в Linux это действительно работает. Размер типа long совпадает с размером указателя. Но такой код несовместим с моделью данных Windows и соответственно его нельзя считать хорошо переносимым. Более правильным решением будет использование типов size_t и ptrdiff_t.

Разработчики Windows в качестве альтернативы size_t и ptrdiff_t могут использовать типы DWORD_PTR, SIZE_T, SSIZE_T и так далее. Но желательно также ограничиваться типами size_t и ptrdiff_t.

Безопасность типов ptrdiff_t и size_t в адресной арифметике

Проблемы адресной арифметики стали активно проявлять себя с началом освоения 64-битных систем. Наибольшее число проблем при переносе 32-битных приложений на 64-битные системы связанно с использованием неподходящих для работы с указателями и массивами типов, таких как int и long. Этим проблемы переноса приложений на 64-битные системы не ограничивается, но большинство ошибок связаны именно с адресной арифметикой и работой с индексами.

Возьмем самый простой пример:

size_t n = ...;
for (unsigned i = 0; i < n; i++)
  a[i] = 0;

Если мы работаем с массивом, состоящим более чем из UINT_MAX элементов, то данный код является некорректным. При этом выявить ошибку и предсказать поведение данного кода не так просто. Отладочная (debug) версия зависнет, но редко кто в отладочной версии будет обрабатывать гигабайты данных. А вот рабочая (release) версия в зависимости от настроек оптимизации и особенностей кода, может как зависнуть, так и неожиданно корректно заполнить все ячейки массива, создавая иллюзию корректной работы. В результате в программе появляются плавающие ошибки, возникающие или пропадающие после малейшего изменения кода. Подробнее о таких фантомных ошибках и их опасностях можно познакомиться в статье "64-битный конь, который умеет считать" [1].

Пример еще одной дремлющей ошибки, которая проявит себя при определенном сочетании входных данных (значении переменных A и B):

int A = -2;
unsigned B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B); //Error
printf("%i\n", *ptr);

Данный код будет успешно выполняться в 32-битном варианте и печатать на экране число "3". После компиляции 64-битном режиме при выполнении кода возникнет сбой. Рассмотрим последовательность выполнения кода и причину ошибки:

  • Переменная A типа int приводится к типу unsigned;
  • Происходит сложение A и B. В результате мы получаем значение 0xFFFFFFFF типа unsigned;
  • Вычисляется выражение "ptr + 0xFFFFFFFFu". Результат зависит от размерности указателя на данной платформе. В 32-битной программе, выражение будет эквивалентно "ptr - 1" и мы успешно распечатаем число 3. В 64-битной программе к указателю прибавится значение 0xFFFFFFFFu, в результате чего указатель окажется далеко за пределами массива.

Приведенные ошибки можно легко избежать, используя тип size_t или ptrdiff_t. В первом случае, если тип переменной "i" будет size_t, то не возникнет зацикливания. Во втором, если мы используем типы size_t или ptrdiff_t для переменных "A" и "B", то корректно распечатаем число "3".

Сформулируем совет: везде, где присутствует работа с указателями или массивами следует использовать типы size_t и ptrdiff_t.

Более подробно с тем, каких ошибок можно избежать, используя типы size_t и ptrdiff_t можно познакомиться в следующих статьях:

Быстродействие кода, использующего типы ptrdiff_t и size_t

Использование типов ptrdiff_t и size_t в адресной арифметике помимо повышения надежности кода может дать дополнительный выигрыш в производительности. Например, использование в качестве индекса типа int, размерность которого отличается от размерности указателя приводит к тому, что в двоичном коде будут присутствовать дополнительные команды преобразования данных. Речь идет о 64-битном коде, в котором размер указателей стал равен 64-битам, а размер типа int остался 32-битным.

Показать короткий пример преимущества size_t над unsigned не простая задача. Чтобы быть объективным необходимо использовать оптимизирующие возможности компилятора. А два варианта оптимизированного кода часто становятся слишком непохожим, чтобы легко было продемонстрировать отличие. Попытка создать нечто близкое к простому примеру увенчалась успехом только с шестой попытки. И все равно пример не идеален, так как показывает не лишние преобразование типов данных, о которых сказано выше, а то, что компилятор смог построить более эффективный код при использовании типа size_t. Рассмотрим код программы, переставляющий элементы массива в обратном порядке:

unsigned arraySize;
...
for (unsigned i = 0; i < arraySize / 2; i++)
{
  float value = array[i];
  array[i] = array[arraySize - i - 1];
  array[arraySize - i - 1] = value;
}

В примере переменные "arraySize" и "i" имеют тип unsigned. Этот тип легко можно заменить на тип size_t и сравнить небольшой участок ассемблерного кода, показанный на рисунке 1.

https://import.viva64.com/docx/blog/size_t_and_ptrdiff_t_ru/image3.png

Рисунок N1. Сравнение 64-битного ассемблерного кода при использовании типов unsigned и size_t

Компилятор смог построить более лаконичный код, когда использовал 64-битные регистры. Автор не берется утверждать, что код, созданный при использовании типа unsigned (текст слева), будет работать медленнее, чем код с использованием size_t (текст справа). Сравнить скорость выполнения кода на современных процессорах крайне сложная задача. Но из примера видно, что когда компилятор работает с массивами, используя 64-битные типы, он может строить более короткий и быстрый код.

По личному опыту автора, грамотная замена типов int и unsigned на ptrdiff_t и size_t может дать на 64-битной системе дополнительный прирост производительности до 10%. С одним из примеров увеличения скорости от использования типов ptrdiff_t и size_t можно познакомиться в четвертой главе статьи "Разработка ресурсоемких приложений в среде Visual C++" [5].

Рефакторинг кода с целью перехода на типы ptrdiff_t и size_t

Как читатель уже убедился, использование типов ptrdiff_t и size_t имеет ряд преимуществ для 64-битных программ. Но и заменить, скажем все unsigned на size_t - не является выходом. Во-первых, это не гарантирует корректность программы на 64-битной системы. Во-вторых, скорее всего из-за такой замены возникнут новые ошибки, нарушится совместимость форматов данных и так далее. Не стоит забывать, что после такой замены существенно возрастет и объем потребляемой программой памяти. Причем увеличение объема требуемой памяти может замедлить работу приложения, так как в кэш будет помещаться меньше объектов, с которыми идет работа.

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

  • Использовать специализированные инструменты, такие как Viva64, входящий в состав PVS-Studio. Viva64 это статический анализатор кода, обнаруживающий места, где рационально изменить типы данных, чтобы программа была корректна и эффективно работала на 64-битных системах. Подробнее смотрите "Учебное пособие по PVS-Studio" [6].
  • Если 32-битную программу не планируется адаптировать для 64-битных систем, то и нет смысла заниматься рефакторингом типов данных. 32-битная программа не получит никаких преимуществ от использования типов ptrdiff_t и size_t.

Библиографический список



Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;

Андрей Карпов
Статей: 396


Найденные ошибки

Проверено проектов
409
Собрано ошибок
14 072

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;
Этот сайт использует куки и другие технологии, чтобы предоставить вам более персонализированный опыт. Продолжая просмотр страниц нашего веб-сайта, вы принимаете условия использования этих файлов. Если вы не хотите, чтобы ваши данные обрабатывались, пожалуйста, покиньте данный сайт. Подробнее →
Принять