Статья поможет читателю разобраться, что представляют собой типы size_t и ptrdiff_t, для чего они нужны и когда целесообразно их использование. Статья будет интересна разработчикам, начинающим создание 64-битных приложений, где использование типов size_t и ptrdiff_t обеспечивает быстродействие, возможность работы с большими объёмами данных и переносимость между разными платформами.
Сразу заметим, что данные в статье определения и рекомендации относятся к наиболее распространённым на данный момент архитектурам (IA-32, Intel 64, IA-64) и могут быть неточны по отношению к экзотическим архитектурам.
Типы size_t и ptrdiff_t были созданы для того, чтобы осуществлять корректную адресную арифметику. Долгое время было принято считать, что размер int совпадает с размером машинного слова (разрядностью микропроцессора) и его можно использовать в качестве индексов для хранения размеров объектов или указателей. Соответственно, адресная арифметика также строилась с использованием типов int и unsigned. Тип int используется в большинстве обучающих материалов по программированию на C и C++ в телах циклов и в качестве индексов. Практически каноническим выглядит пример следующего вида:
for (int i = 0; i < n; i++)
a[i] = 0;
С развитием микропроцессоров и ростом их разрядности стало нерационально дальнейшее увеличение размерностей типа int. Причин для этого много: экономия используемой памяти, максимальная совместимость и так далее. В результате появилось несколько моделей данных, описывающих соотношение размеров базовых типов для языка C и C++. В таблице N1 приведены основные модели данных и перечислены наиболее популярные системы, использующие их.
Таблица N1. Модели данных (data models)
Как видно из таблицы, не так просто выбрать тип переменной для хранения указателя или размера объекта. Чтобы наиболее красиво решить эту проблему, и появились типы size_t и ptrdiff_t. Они гарантированно могут использоваться для адресной арифметики. Теперь каноническим должен стать следующий код:
for (size_t i = 0; i < n; i++)
a[i] = 0;
Именно он может обеспечить надёжность, переносимость, быстродействие. Почему — станет ясно из дальнейшего текста статьи.
size_t – это специальный беззнаковый целочисленный тип, определённый в стандартных библиотеках языков С и С++. Является типом результата, возвращаемого оператором sizeof и alignof.
Максимально допустимым значением типа size_t является константа SIZE_MAX.
Размер size_t выбирается таким образом, чтобы в него можно было записать максимальный размер теоретически возможного массива или объекта. Другими словами, количество бит в size_t равно количеству бит, которое требуется для хранения максимального адреса в памяти машины. Например, на 32-битной системе size_t будет занимать 32-бита, на 64-битной — 64-бита. В свою очередь это означает, что в тип size_t может быть безопасно помещён указатель (исключение составляют платформы с сегментной адресацией и указатели на функции-члены классов).
Данный тип перекладывает заботу о возможном разном поведении целочисленных переменных при смене платформы с плеч программиста на реализацию стандартной библиотеки. Поэтому использование типа size_t безопаснее и эффективнее, чем использование обычных беззнаковых целочисленных типов:
В языке С тип size_t объявлен в заголовочных файлах <stddef.h>, <stdlib.h>, <string.h>, <wchar.h>, <uchar.h>, <time.h> и <stdio.h>. В языке С++ его декларация находится в файлах <cstddef>, <cstdlib>, <cstring>, <cwchar>, <cuchar>, <ctime> и <cstdio>. Тип size_t размещён в глобальном пространстве имён и в std. Стандартные заголовочные файлы языка C для обеспечения обратной совместимости также могут включаться в С++ программы.
Примечание. Ещё существует тип rsize_t. Он является синонимом size_t. Но он предназначен для хранения размера одиночного объекта. Другими словами, используя rsize_t, программист подчёркивает, что работает с размером одного-единственного объекта. При этом максимальный размер одиночного объекта задаётся константой RSIZE_MAX.
ptrdiff_t – это специальный знаковый целочисленный тип, определённый в стандартных библиотеках языков С и С++. Является типом результата вычитания указателей. Поведение типа схоже с size_t: на 32-битной системе размер ptrdiff_t будет 32 бита, на 64-битной – 64 бит.
Также при работе с контейнерами стандартной библиотеки результат вычитания двух итераторов имеет тип difference_type используемого контейнера, который, в зависимости от стандартной библиотеки, часто равен ptrdiff_t.
Тип ptrdiff_t часто используется для адресной арифметики и индексации массивов, если возможны отрицательные значения. В программах, использующих для этого обычные целочисленные типы (int), может возникать неопределённое поведение. Например, если значение индекса превышает INT_MAX.
Для массивов меньших, чем PTRDIFF_MAX, ptrdiff_t ведёт себя как аналог size_t: может хранить размер массива любого типа и на большинстве платформ является синонимом intptr_t. Однако если массив достаточно большой (больше PTRDIFF_MAX, но меньше SIZE_MAX) и разница его указателей не может быть представлена в виде ptrdiff_t, то результат вычитания таких указателей не определён.
В языке С тип ptrdiff_t объявлен в заголовочном файле <stddef.h>. В языке С++ его декларация находится в <cstddef> и размещается в глобальном пространстве имён и в std. Стандартные заголовочные файлы языка C для обеспечения обратной совместимости также могут включаться в С++ программы.
Типы size_t и ptrdiff_t позволяют писать переносимый код. Размер size_t и ptrdiff_t всегда совпадают с размером указателя. По этой причине именно эти типы следует использовать в качестве индексов больших массивов для хранения указателей и арифметики с указателями.
Разработчики Linux приложений часто используют для этих целей тип long. В рамках 32-битных и 64-битных моделей данных, принятых в Linux, это действительно работает. Размер типа long совпадает с размером указателя. Но такой код несовместим с моделью данных Windows, и, соответственно, его нельзя считать хорошо переносимым. В модели LLP64 (Windows x64) тип long остался 32-битным. Более правильным решением будет использование типов size_t и ptrdiff_t.
Разработчики в экосистеме Windows в качестве альтернативы size_t и ptrdiff_t могут использовать типы DWORD_PTR, SIZE_T, SSIZE_T и так далее. Но желательно ограничиваться типами size_t, ptrdiff_t, uintptr_t, intptr_t для большей совместимости.
Проблемы адресной арифметики стали активно проявлять себя с началом освоения 64-битных систем. Наибольшее число проблем при переносе 32-битных приложений на 64-битные системы связанно с использованием неподходящих для работы с указателями и массивами типов, таких как int и long. Этим проблемы переноса приложений на 64-битные системы не ограничиваются, но большинство ошибок связаны именно с адресной арифметикой и работой с индексами. Более подробно проблемы переноса кода раскрыты в уроках по разработке 64-битных приложений на языке C и C++ [1].
Рассмотрим простой пример:
size_t n = ...;
for (int i = 0; i < n; i++)
a[i] = 0;
Если мы работаем с массивом, состоящим более, чем из INT_MAX элементов, то данный код является некорректным. При переполнении знаковой переменной возникает неопределённое поведение. В отладочной (debug) версии программы скорее возникнет Access Violation, когда значение индекса переполнится. А вот (release) версия в зависимости от настроек оптимизации и особенностей кода, может, например, неожиданно корректно заполнить все ячейки массива, создавая иллюзию корректной работы! В результате в программе появляются плавающие ошибки, возникающие или пропадающие после малейшего изменения кода. Подробнее о таких фантомных ошибках и их опасностях можно познакомиться в статье "64-битный конь, который умеет считать" [2].
Пример еще одной дремлющей ошибки, которая проявит себя при определенном сочетании входных данных (значении переменных 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-битном режиме при выполнении кода возникнет сбой. Рассмотрим последовательность выполнения кода и причину ошибки:
Приведённые ошибки можно легко избежать, используя тип 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 в адресной арифметике помимо повышения надёжности кода может дать дополнительный выигрыш в производительности. Например, использование в качестве индекса типа 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.
Рисунок N1. Сравнение 64-битного ассемблерного кода при использовании типов unsigned и size_t
Компилятор смог построить более лаконичный код, когда использовал 64-битные регистры. Автор не берётся утверждать, что код, созданный при использовании типа unsigned (текст слева), будет работать медленнее, чем код с использованием size_t (текст справа). Сравнить скорость выполнения кода на современных процессорах — крайне сложная задача. Но из примера видно, что, когда компилятор работает с массивами, используя 64-битные типы, он может строить более короткий и быстрый код.
По личному опыту автора, грамотная замена типов int/unsigned на ptrdiff_t/size_t может дать на 64-битной системе дополнительный прирост производительности до 10%. С одним из примеров увеличения скорости от использования типов ptrdiff_t и size_t можно познакомиться в четвёртой главе статьи "Разработка ресурсоемких приложений в среде Visual C++" [7].
Как читатель уже убедился, использование типов ptrdiff_t и size_t имеет ряд преимуществ для 64-битных программ. Но и заменить, скажем, все unsigned на size_t не является выходом. Во-первых, это не гарантирует корректность программы на 64-битной системе. Во-вторых, скорее всего, из-за такой замены возникнут новые ошибки, нарушится совместимость форматов данных и так далее. Не стоит забывать, что после такой замены может существенно возрасти и объем потребляемой программой памяти. Причём увеличение объема требуемой памяти может замедлить работу приложения, так как в кэш будет помещаться меньше объектов, с которыми идет работа.
Следовательно, внедрение в старый код типов ptrdiff_t и size_t является задачей постепенного вдумчивого рефакторинга, требующего большого количества времени. Фактически, необходимо просмотреть весь код и внести необходимые исправления. Такой подход на практике является слишком дорогостоящим и неэффективным. Можно предложить 2 варианта:
0