metrica
Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
close form

Заполните форму в два простых шага ниже:

Ваши контактные данные:

Шаг 1
Поздравляем! У вас есть промокод!

Тип желаемой лицензии:

Шаг 2
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности
close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS‑Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

Вебинар: Трудности при интеграции SAST, как с ними справляться - 04.04

>
>
>
Что такое size_t и ptrdiff_t

Что такое size_t и ptrdiff_t

21 Сен 2009

Статья поможет читателю разобраться, что представляют собой типы 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 приведены основные модели данных и перечислены наиболее популярные системы, использующие их.

a0050_size_t_and_ptrdiff_t_ru/image1.png

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

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

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

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

Тип size_t

size_t – это специальный беззнаковый целочисленный тип, определённый в стандартных библиотеках языков С и С++. Является типом результата, возвращаемого оператором sizeof и alignof.

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

Размер size_t выбирается таким образом, чтобы в него можно было записать максимальный размер теоретически возможного массива или объекта. Другими словами, количество бит в size_t равно количеству бит, которое требуется для хранения максимального адреса в памяти машины. Например, на 32-битной системе size_t будет занимать 32-бита, на 64-битной — 64-бита. В свою очередь это означает, что в тип size_t может быть безопасно помещён указатель (исключение составляют платформы с сегментной адресацией и указатели на функции-члены классов).

Данный тип перекладывает заботу о возможном разном поведении целочисленных переменных при смене платформы с плеч программиста на реализацию стандартной библиотеки. Поэтому использование типа size_t безопаснее и эффективнее, чем использование обычных беззнаковых целочисленных типов:

  • Данный тип позволяет писать циклы и счётчики, не беспокоясь о возможном переполнении при смене платформы. Например, когда количество необходимых итераций превышает UINT_MAX;
  • Поскольку стандартом гарантируется, что size_t может вместить в себя размер максимально возможного объекта в системе, этот тип используют для хранения размеров объектов;
  • Из-за этого же свойства size_t используют для индексации массивов. В отличие от обычных базовых целочисленных типов, size_t гарантирует, что значение индекса не может быть больше SIZE_MAX;
  • Так как в size_t обычно может быть безопасно помещён указатель, его используют для адресной арифметики. Однако для этих целей лучше подходит другой беззнаковый целочисленный тип uintptr_t, само название которого отражает его предназначение;
  • Компилятор может построить более простой и, следовательно, более быстрый код, в котором будут отсутствовать лишние преобразования 32-битных и 64-битных данных.

В языке С тип 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

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 позволяют писать переносимый код. Размер 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 для большей совместимости.

Безопасность типов ptrdiff_t и size_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-битном режиме при выполнении кода возникнет сбой. Рассмотрим последовательность выполнения кода и причину ошибки:

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

Приведённые ошибки можно легко избежать, используя тип 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.

a0050_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++" [7].

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

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

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

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

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

Популярные статьи по теме


Комментарии (0)

Следующие комментарии next comments
close comment form