65.9K
CodeProject 正在变化。 阅读更多。
Home

64 位世界中的 32 位指针

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.18/5 (6投票s)

2008年8月25日

CPOL

3分钟阅读

viewsIcon

76935

downloadIcon

224

如果您不需要访问 TB 级的 RAM,64 位指针是浪费的

引言

64 位非常棒 - 我们不再局限于仅 2GB (每个 Win32 进程) - 现在您可以寻址完整的 8TB 内存(每个 Win64 进程)... 如果您的应用程序确实需要 8TB 内存,那就太好了。

如果您不需要完整的地址空间,64 位可能会造成浪费:指针占用双倍的内存,这实际上可能导致应用程序更慢,尤其是在使用指针密集型数据结构(如二叉树)时(更大的结构占用更多的缓存空间)。

本文介绍了一种简单的方案,允许使用 32 位“指针”来访问 64 位机器上高达 64GB 的内存。

在 32 位中编码 64 位指针

sptr 的主要思想是数据对齐 - 现代处理器在访问“对齐”地址时感觉更舒服:在地址 0xC0001 处访问 char 意味着比在 0xC0000 处访问 int 需要更多工作。

malloc() (为 new 做繁重工作) 仅返回对齐的地址 (参见 http://msdn.microsoft.com/en-us/library/ycsb6wwf.aspx) - malloc() 返回的指针在 Visual Studio 上将始终与 16 字节边界对齐 - 最低的 4 位将始终为 0。

因此,至少对于我们在堆上分配的指针,我们可以简单地“删除”最低的 4 位(通过右移)而不会丢失信息。

如果所有内存地址都是从“底部”分配的(最高位也为 0),我们可以按如下方式编码指针

#include <stddef.h>  // for uintptr_t
typedef unsigned __int32 uint32_t;

uint32_t encode(void* p)
{
  uintptr_t i = reinterpret_cast<uintptr_t>(p) >> 4;
  return static_cast<uint32_t>(i);
}

(uintptr_t 是一种整数数据类型,保证与指针的大小相同。)

这会将范围 0x00 0000 0000 - 0x10 0000 0000 中的任何(16 字节对齐的)地址编码为 32 位整数。

但是,操作系统可以自由地将物理内存映射到其他虚拟内存段中 - 例如,将内核内存“在顶部”映射,因此最好自己保留这样的映射

#include <boost/thread/mutex.hpp>
#include <boost/thread/locks.hpp>

class sptr_base
{   
protected:
  static const uint32_t ALIGNMENT_BITS = 4;
  static const uint32_t ALIGNMENT = (1<<ALIGNMENT_BITS);
  static const uintptr_t ALIGNMENT_MASK = ALIGNMENT - 1;

protected:
  static uintptr_t     _seg_map[ALIGNMENT];
  static uintptr_t     _segs;
  static boost::mutex  _m;
  inline static uintptr_t ptr2seg(uintptr_t p)
  {
    p &= ~0xFFFFFFFFULL; // Keep only high part
    uintptr_t s = _segs;
    uintptr_t i = 0;
    for (; i < s; ++i)
      if (_seg_map[i] == p)
    return i;

    // Not found - now we do it the "right" way (mutex and all)
    boost::lock_guard<boost::mutex> lock(_m);
    for (i = 0; i < s; ++i)
      if (_seg_map[i] == p)
        return i;

    i = _segs++;
    if (_segs > ALIGNMENT)
      throw bad_segment("Segment out of range");

    _seg_map[i] = p;
    return i;
  }
};

ptr2seg() 将指针的高位映射到几个段之一。该代码避免在读取映射时使用 mutex,基于整数赋值的原子性。

那么,实际的指针编码/解码如下

#include <boost/pool/detail/singleton.hpp>
typedef boost::details::pool::singleton_default<segment_mapper> the_segment_mapper;

uint32_t encode(const_native_pointer_type ptr)
{
  uintptr_t p = reinterpret_cast<uintptr_t>(ptr);
  if ((p & ALIGNMENT_MASK) != 0)
    throw bad_alignment("Pointer is not aligned");
  return (uint32_t)(ptr2seg(p) + p);
}

inline native_pointer_type decode(uint32_t e)
{
   uintptr_t el = e;
   uintptr_t ptr = (_seg_map[el & ALIGNMENT_MASK] + el) & ~ALIGNMENT_MASK;
   return reinterpret_cast<native_pointer_type>(ptr);
}

sptr

sptr 为“压缩”指针提供了类似指针的接口。 它允许以类似于实际指针的方式访问我们的“指针”

sptr<int> p = new int;
*p = 5;
delete p;

// Conversions from/to pointers and pointer arithmetics work
int* q = new int;
p = q;
p++;
q = p;

限制

sptr 有几个限制(这可能使其在许多地方不适用)。

首先,sptr 无法访问整个 64 位地址空间(duh!)。它仅适用于需要略多于 32 位地址空间的应用程序(服务器机器通常限制为 16GB-32GB 内存;台式机通常无法处理超过 8GB 的内存)。

其次,尽管在堆上分配的地址将正确对齐,但指针算术可能会使事情复杂化

sptr<char> buf = new char[100];

可以正常工作,因为 new 返回一个对齐的地址。

但是,类似以下的情况

for (sptr<char> p = buf; p != buf + 100; ++p)
   *p = '0';

将无法工作,因为 p 现在将指向一个未对齐的地址(sptr 实现将在编译时断言)。

但是,在大多数情况下,可以使用标准指针

for (char* p = buf; p != buf + 100; ++p)
   *p = '0';

第三,谨慎地将 sptr 与多重继承混合使用。

例如,给定

struct A { char i; };
struct B { int j; };
struct C : public A, public B { };

这可能会或可能无法工作(取决于结构对齐方式)

sptr<C> c = new C;
sptr<B> b = c;

这是因为 b 指向与 c 相同的地址(这不是讨论多重继承的地方,但您可以尝试以下代码来说服自己)

C* c = new C;
B* b = c; 
std::cout << ((uintptr_t)b - (uintptr_t)c) << std::endl;

通过标准指针访问对象没有问题

sptr<C> c = new C;
B* b = c;

性能

代码中提供了一个小型基准测试,它实现了一个简单的链表,该链表被重复遍历。

列表的节点是

struct node
{
   int val;
   node_ptr prev;
   node_ptr next;
};

因此,使用标准指针的节点大小为 20 字节,而使用 sptr 的节点大小为 12 字节。

但是,这是以权衡为代价的 - 额外的位洗牌会影响性能 - 在链表基准测试中,标准指针版本将快 1.5 倍。

访问映射占了大部分性能损失。如果可以保证所有地址都是从区域 0x00 0000 0000 - 0x10 0000 0000 分配的,我们可以丢弃映射,从而显着提高性能。

© . All rights reserved.