【问题标题】:How bad is to use integer pointers as unique ids? C++11使用整数指针作为唯一IDS的糟糕? C ++ 11.
【发布时间】:2014-07-30 23:59:03
【问题描述】:

我有一个类,在实例化时需要获取一些唯一的 id 才能工作。 最初我想使用一个分配和递增的静态函数。 我不需要它们是连续的,只要是唯一的。

class A {
    int id_1;
    int id_2;
    int id_3;
public:
    static int last_id=0;
    static int get_id(){ return A::last_id++; }
    ...
    A(){ id_1 = A::get_id(); id_2 = A::get_id(); id_3 = A::get_id(); } 
};

现在,我正在考虑使用多线程。 我认为静态函数将是一个瓶颈,因为我在一开始就构建了这些对象的几十万个实例。 在程序结束之前我不会销毁任何实例,因此在初始化后它们是固定的。 无论如何,它们不是在编译时计算的,因为数量取决于命令行参数。

我想到的另一种方法是使用内存地址,它们至少在一台计算机中是唯一的。

类似:

class A {
    int* id_1;
    int* id_2;
    int* id_3;
public:
    static int last_id=0;
    static int get_id(){ return A::last_id++; }
    ...
    A(){ id_1 = new int(0); id_2 = new int(0); id_3 = new int(0); } 
    ~A() { delete id_1; delete id_2; delete id_3(); }
};

然后我会将标识符读取为指针的地址。

问题:这样使用指针有意义吗?

【问题讨论】:

  • 它们仅在此过程中是唯一的(并且它们可能不按顺序排列)。另外,我看不出new 比在 int 上执行 ++ 更容易成为瓶颈!
  • 据我所知,new 是线程安全的,同样会成为瓶颈
  • 也绝对没有强制将内存指针作为 GUID 的,除非您打算从不释放任何东西。一旦一些内存被释放,它的指针空间就会被备份。
  • 我觉得你在真正知道瓶颈是什么之前就已经在优化了。增加一个整数会非常便宜!建议:使用std::atomic<int> 而不是int,这样它就可以与多个线程一起使用。
  • @dvicino: 关心,this + 1 已经在this 对象之外(相当于&(this[1]))。你的意思是reinterpret_cast<char*>(this) + n。当n >= sizeof(*this) 时,后者将在对象之外。

标签: c++ multithreading pointers c++11


【解决方案1】:

您的原始解决方案确实离您不远了。 不要过早优化!增加int 非常便宜!我唯一的建议是使用std::atomic<int> 而不是int

class A {
    int id_1;
    int id_2;
    int id_3;

    static int get_id() {
        static std::atomic<int> next_id(1);
        return ++next_id;
    }

public:
    A() :
        id_1(get_id()),
        id_2(get_id()),
        id_3(get_id())
    { }

    // deal with copying by disabling
    A(const A&) = delete;
    A& operator=(const A&) = delete;

    // move is okay
    A(A&&) noexcept = default;
    A& operator=(A&&) noexcept = default;
};

假设您创建的2^31/3 实例不超过A,则不必担心溢出。

【讨论】:

  • 我喜欢你的方法(似乎我们同时提出了类似的建议)!但是,如果唯一 id 是一个 int,是否真的需要禁用复制?仅仅处理特殊情况(即为复制构造函数创建一个新的 id 并保持目标 id 不变以进行分配)难道不够吗?
  • 可能是的,但我不知道@dvicino 的对象有哪些其他成员(假设A 不只是3 个ID)。
  • @TravisGockel 我使用这种方法测量了 200.000 个对象的构造,并且在 2010 电脑中使用 -O0 编译(在单线程中)花费了 0.009 秒。你对进一步优化这个没有意义。它是一次性操作,并且已经相当快了。我会在下次之前测量。
  • 如果您不关心 id 的顺序,您甚至可以更进一步,使用具有宽松内存顺序的 fetch_add。它在 x86 上可能不会有任何区别,但是对于一些较弱的架构,它可以显着加快增量。
  • 如果我们真的想避免拥塞,请改用线程本地计数器。只要我们知道线程数,这是安全的,并且可以避免任何同步。
【解决方案2】:

我之前在 C 语言中使用过类似以下内容的快速破解 - 当我需要一些仅在进程生命周期内唯一的唯一值时。

常量.h

extern const void * id_1;
extern const void * id_2;

常量.c

const void * id_1 = &id_1;
const void * id_2 = &id_2;

不用担心清理等,因为它们是外部全局变量。

在 C++ 中,对于类实例,您可以使用相同的想法,但要本地化到实例:

class A {
    void* id_1;
    void* id_2;
    void* id_3;
public:
    A(){ id_1 = &id_1; id_2 = &id_2; id_3 = &id_3; } 
};

请注意,id 仅在实例存在时是唯一的 - 但您说过它们仅在应用程序退出时被分配 - 所以您应该没问题。

注意:我确实认为这是一种 hack,并且使用 C++11s std::atomic 上面的 Travis Glockel 提供的解决方案同样简单且更强大。但是,如果没有 C++11 实现 atomic 或添加 atomic 库,就会很麻烦。

【讨论】:

  • OP 表示他们需要数十万个 GUID。
  • @aruisdante:OP可以使用会员地址。
  • 优点是你的技巧避免了额外的新事物。但是,如果您使用会员地址,那么您的技巧就是使用 this 作为唯一 ID,所有问题都与在复制或移动对象时丢失身份相关的所有问题
  • @Christophe - id 的值仅在默认构造函数中设置 - 它们保留在对象的副本中。因此,您不会在副本中丢失身份。即默认情况下副本将共享父级的ID。
  • @Jarod42 OP 使用 new 来创建未重新分配的虚拟整数。但你是对的:在任何情况下,唯一 id(无论是指针还是 int)都需要对复制/移动进行特殊处理,以确保对象的唯一性。
【解决方案3】:

我只是想出了这个,所以我不知道它是否安全!根据需要向班级添加尽可能多的 ID 成员。

struct ID
{
    uintptr_t GetID() const
    {
        return reinterpret_cast<uintptr_t>(this);
    }
};

【讨论】:

  • 这样做的缺点是你的“身份”在复制时发生了变化。
  • @TravisGockel 这可能是一个优势。副本肯定应该与原始 ID 不同吗?
  • 副本在我的特殊情况下需要不同的id,可以在复制构造函数中调整。
  • 如果有一天您将对象存储在向量中,那么当向量容量增加时,该对象就有可能失去其身份。
  • @Christophe 然后动态分配对象并禁用复制和移动。这并不比取this 或其他地方推荐的成员的地址差。
【解决方案4】:

id 的唯一性意味着一个瓶颈,无论是通过使计数器线程安全,还是依靠线程安全 new。

您最好将last_id 设为atomic&lt;int&gt; 并使用运算符++ 将其作为原子操作递增。我相信这将比 new 分配虚拟 int 必须做的所有堆管理工作更有效率。

如果这将成为一个真正的瓶颈,您可以使用稍微不同的方法,使用每个线程本地的 id 生成器,并将本地(即没有种族)id 与线程 id 结合起来(this_thread::get_id() 是唯一的,只要因为线程保持可连接),以便在所有线程中具有唯一的 id。

一种变体是使用前面建议的静态原子 int,但一次分配多个 id 的块(并在每个线程中本地管理缓存的 id)。

【讨论】:

    【解决方案5】:

    其他答案主要集中在替代解决方案上,而不是像 OP 所要求的那样询问new 方法的问题所在。

    缺点

    • 您正在分配一个指针以及一个可以保存 Id 的 int 的存储空间。对于几十万个 ID,这是您需要的存储空间的 2-3 倍(尽管仍然相当少)。
    • 使用地址作为唯一 ID 意味着您无法获得可预测的排序。例如,如果您将 id 放入 map&lt;&gt; 然后沿着它走,即使系统具有相同的输入,您也会得到不同的顺序。这可能会导致不可预知的行为,并使调试变得痛苦。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-10-02
      • 2015-02-25
      • 2010-12-30
      • 2013-05-30
      • 2018-07-09
      • 1970-01-01
      相关资源
      最近更新 更多