Подтвердить что ты не робот

Можно ли надежно хранить несвязанные данные в наименьшем значении бит указателя?

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

Тем не менее, мне все еще интересно узнать, есть ли причина, почему это не сработает.

Ситуация такова: у меня есть класс интеллектуальных указателей с подсчетом ссылок, который я использую везде. В настоящее время он выглядит примерно так (примечание: неполный/упрощенный psuedocode):

class IRefCountable
{
public:
    IRefCountable() : _refCount(0) {}
    virtual ~IRefCountable() {}

    void Ref() {_refCount++;}
    bool Unref() {return (--_refCount==0);}

private:
    unsigned int _refCount;
};

class Ref
{
public:
   Ref(IRefCountable * ptr, bool isObjectOnHeap) : _ptr(ptr), _isObjectOnHeap(isObjectOnHeap) 
   { 
      _ptr->Ref();
   }

   ~Ref() 
   {
      if ((_ptr->Unref())&&(_isObjectOnHeap)) delete _ptr;
   }

private:
   IRefCountable * _ptr;
   bool _isObjectOnHeap;
};

Сегодня я заметил, что sizeof (Ref) = 16. Однако, если я удаляю логическую переменную-член _isObjectOnHeap, sizeof (Ref) уменьшается до 8. Это означает, что для каждого Ref в моей программе есть 7,875 потраченных впустую байтов ОЗУ... и в моей программе много, много Refs.

Ну, это кажется пустой тратой памяти. Но мне действительно нужна эта дополнительная информация (хорошо, юмористируйте меня и принимайте во внимание обсуждение, которое я действительно делаю). И я замечаю, что поскольку IRefCountable является классом, отличным от POD, он (предположительно) всегда будет выделен на адрес памяти с выравниванием по словам. Поэтому наименьший значащий бит (_ptr) всегда должен быть равен нулю.

Что заставляет меня задаться вопросом... есть ли причина, по которой я не могу ИЛИ мой один бит логических данных в наименее значимый бит указателя, и таким образом уменьшить sizeof (Ref) наполовину, не жертвуя какой-либо функциональностью? Я должен был бы быть осторожным с AND из этого бита до разыменования указателя, конечно, что сделало бы ненужные разметки указателя, но это могло бы быть вызвано тем фактом, что Refs теперь меньше, и, следовательно, больше из них может сразу входить в процессорный кеш и т.д.

Это разумная вещь? Или я настраиваю себя на мир боли? И если последнее, как именно было бы больно посещать меня? (Обратите внимание, что это код, который должен работать корректно во всех достаточно современных средах рабочего стола, но его не нужно запускать во встроенных машинах или суперкомпьютерах или что-то подобное экзотическому)

4b9b3361

Ответ 1

Любая причина? Если в последнее время в стандарте ничего не изменилось, представление значения указателя будет определено реализацией. Конечно, возможно, что некоторая реализация где-то может вытащить один и тот же трюк, определяя эти неиспользуемые младшие биты для своих целей. Еще более возможно, что в некоторой реализации могут использоваться указатели слов, а не байт-указатели, поэтому вместо двух смежных слов, находящихся на "адресах" 0x8640 и 0x8642, они будут находиться в "адресах" 0x4320 и 0x4321.

Один сложный способ решения проблемы состоит в том, чтобы сделать Ref a (de facto) абстрактным классом, и все экземпляры будут фактически экземплярами RefOnHeap и RefNotOnHeap. Если есть много Refs, дополнительное пространство, используемое для хранения кода и метаданных для трех классов, а не для одного, было бы компенсировано экономией пространства при том, что каждый Ref был вдвое меньше. (не будет работать слишком хорошо, компилятор может опустить указатель vtable, если нет виртуальных методов, а введение виртуальных методов добавит 4-х или 8-байты обратно в класс).

Ответ 2

Проблема здесь в том, что она полностью зависит от машины. Это не то, что часто можно увидеть в коде C или С++, но это было сделано много раз в сборке. Старые Lisp интерпретаторы почти всегда использовали этот трюк для хранения информации о типе в младших бит. (Я видел int в коде C, но в проектах, которые были реализованы для конкретной целевой платформы.)

Лично, если бы я пытался написать переносимый код, я бы, вероятно, этого не сделал. Дело в том, что он почти наверняка будет работать на "всех разумно современных средах рабочего стола". (Конечно, это будет работать на всех, о которых я могу думать.)

Многое зависит от характера вашего кода. Если вы его поддерживаете, и никто больше не будет иметь дело с "миром боли", тогда это может быть хорошо. Вам нужно будет добавить ifdef для любой нечетной архитектуры, которая вам может понадобиться позже. С другой стороны, если вы выпускаете его в мир как "портативный" код, это может вызывать беспокойство.

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

Само собой разумеется, что вам не нужно писать какой-либо другой код, который предполагает, что sizeof(Ref) равен 8, а не 16. Если вы используете модульные тесты, запустите их с обеих версий.

Ответ 3

Если вы хотите использовать только стандартные средства и не полагаться на какую-либо реализацию, тогда с С++ 0x есть способы выражения выравнивания (здесь есть недавний вопрос Я ответил). Там также std::uintptr_t надежно получить неподписанный интегральный тип, достаточно большой, чтобы удерживать указатель. Теперь гарантируется, что преобразование из типа указателя в std::[u]intptr_t и обратно к тому же типу дает исходный указатель.

Я полагаю, вы могли бы утверждать, что если вы можете вернуть исходный std::intptr_t (с маскировкой), вы можете получить исходный указатель. Я не знаю, насколько это было бы разумно.

[edit: думать об этом там не гарантирует, что выровненный указатель принимает какую-либо конкретную форму при преобразовании в интегральный тип, например. один с некоторыми разрядами. вероятно, слишком много растягивается здесь]

Ответ 4

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

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

Ref(IRefCountable * ptr) : _ptr(ptr) 
{
  if(ptr != 0) 
    _ptr->Ref();
}

Из кода я чувствую, что подсчет ссылок нужен только тогда, когда объект находится на куче. Для автоматических объектов вы можете просто передать 0 в class Ref и поместить соответствующие нулевые проверки в конструктор/деструктор.

Ответ 5

Вы задумывались над хранилищем вне класса?

В зависимости от того, хотите ли вы (или нет) беспокоиться о многопоточности и контролировать реализацию нового /delete/malloc/free, возможно, стоит попробовать.

Точка будет заключаться в том, что вместо того, чтобы увеличивать локальный счетчик (локальный для объекта), вы должны поддерживать "адресный" адрес карты → счет, который надменно игнорирует переданные адреса, которые находятся за пределами выделенной области (например, стек).

Это может показаться глупым (в MT есть место для конкуренции), но он также неплохо работает с только для чтения, поскольку объект не "модифицируется" только для подсчета.

Конечно, я не имею представления о производительности, которую вы могли бы надеяться достичь: p