Статьи

О size_t и ptrdiff_t

Аннотация

Статья поможет читателям понять, что такое типы 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. Модели данных

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

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

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

тип size_t

Тип size_t является базовым целым типом без знака языка C / C ++. Это тип результата, возвращаемого оператором 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 является базовым целочисленным типом со знаком языка C / C ++. Размер типа выбирается таким образом, чтобы он мог хранить максимальный размер теоретически возможного массива любого типа. В 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, легко переносим. Размеры size_t и ptrdiff_t всегда совпадают с размером указателя. Из-за этого именно эти типы должны использоваться в качестве индексов для больших массивов, для хранения указателей и арифметики указателей.

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

В качестве альтернативы size_t и ptrdiff_t разработчики Windows могут использовать типы 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 элементов, этот код неверен. Нелегко обнаружить ошибку и предсказать поведение этого кода. Отладочная версия будет зависать, но вряд ли кто-то будет обрабатывать гигабайты данных в отладочной версии. И релиз-версия, в зависимости от настроек оптимизации и особенностей кода, может либо зависнуть, либо внезапно заполнить все ячейки массива правильно, создавая иллюзию правильной работы. В результате в программе появляются плавающие ошибки, возникающие и исчезающие при малейшем изменении кода. Чтобы узнать больше о таких фантомных ошибках и их опасных последствиях, смотрите статью « 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-битном режиме произойдет сбой при выполнении кода. Давайте рассмотрим последовательность выполнения кода и причину ошибки:

  • Переменная типа int приводится к типу без знака;
  • А и Б суммируются. В результате мы получаем значение 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 над типом без знака. Чтобы быть объективным, мы должны использовать оптимизирующие способности компилятора. И два варианта оптимизированного кода часто становятся слишком разными, чтобы показать эту самую разницу. Нам удалось создать что-то вроде простого примера только с шестой попытки. И все же пример не идеален, потому что он демонстрирует не те ненужные преобразования типов данных, о которых мы говорили выше, а то, что компилятор может создавать более эффективный код при использовании типа 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-битных регистров. Я не утверждаю, что код, созданный с использованием типа без знака, будет работать медленнее, чем код, использующий size_t. Сравнение скорости выполнения кода на современных процессорах — очень сложная задача. Но из примера видно, что когда компилятор работает с массивами с использованием 64-битных типов, он может создавать более короткий и быстрый код.

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

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

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

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

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

Рекомендации

  1. 64-битная лошадь, которая умеет считать. http://www.viva64.com/art-1-2-377673569.html
  2. 20 вопросов портирования кода C ++ на 64-битную платформу. http://www.viva64.com/art-1-2-599168895.html
  3. Безопасность 64-битного кода. http://www.viva64.com/art-1-2-416605136.html
  4. Обнаружение ловушек при переносе кода C и C ++ в 64-битную Windows. http://www.viva64.com/art-1-2-2140958669.html
  5. Разработка ресурсоемких приложений в Visual C ++. http://www.viva64.com/art-1-2-2014169752.html
  6. Евгений Рыжков. Учебник по PVS-Studio. http://www.viva64.com/art-4-2-747004748.html