实现 operator[] / iterator 来处理性能问题的方法是啥?

Posted

技术标签:

【中文标题】实现 operator[] / iterator 来处理性能问题的方法是啥?【英文标题】:What is the approach to implement operator[] / iterator to deal with performance issue?实现 operator[] / iterator 来处理性能问题的方法是什么? 【发布时间】:2021-01-13 21:06:39 【问题描述】:

假设我有一个自定义容器MyList,如下所示。

template<class T>
class MyList

public:
    const T& get (size_t index) const
    
        Block& block = getBlock (index);
        return block.get (index - block.startIndex);
    
    void set (size_t index, const T& value)
    
        Block& block = getBlock (index);
        block.dirty = true;
        block.set (index - block.startIndex, value);
    
    // ...
;

块基本上是从磁盘加载的缓存数据块(可以有 2-3 个这样的 LRU 块)。如果一个块是脏的,当它不是内存时,它的数据将被写入磁盘。

我的问题如下。

    重载 operator[] 无法使用正确的 operator[]。我重载了以下两个:

     T& operator[](size_t index)
     
         Block& block = getBlock (index);
         block.dirty = true;
         return block.get (index - block.startIndex);
     
     const T& operator[](size_t index) const
     
         return get (index);
     
    

但是,如果MyList 不是const 版本,g++ 会调用第一个。也就是说,对于如下代码:

MyList<int> list;
// ...
int myValue = list[0];

调用的 operator[] 总是第一个设置脏位的。这不是我想要的。

我做错了吗?虽然我当然可以调用get() 或使用const_iterator,但我担心MyList 的用户会在不希望的情况下使用operator[]。有没有更好的运算符重载方法没有这个问题?

    迭代器呢?我在迭代器中遇到了operator* 的类似问题。如果迭代器本身不是const,则始终调用第一个。
T& operator* ();
const T& operator* () const;
    我的目标是在这个容器上运行sort/nth_element/等C++ STL算法。如果我们考虑 C++ STL 算法的搜索阶段和交换阶段,在我看来,如果使用正确的迭代器,如果数据大部分已经排序,那么排序速度可能会更快。
MyList<int> list;
// ...
std::sort (list.begin (), list.end ());

如果数据没有改变,有没有办法最小化设置脏位的块数?

你能提供任何关于看/关注/学习什么的建议吗?或者这是语言或编译器的限制?

【问题讨论】:

“但是,如果 MyList 不是 const 版本,G++ 似乎会调用第一个。” - 这就是 const 关键字在应用于类方法。在重载解析期间,编译器将根据调用实例的 const-ness 选择适当的函数。 在非常量设置中,operator[] 传统上用于 gettingsetting。也就是说,myValue = list[0] list[0] = myValue. 副手:返回一个代理对象,当分配给T类型的对象时,通过operator T展开(并且从不设置dirty),并且当分配一个@类型的元素时987654344@(operator=(T t) 调用)进行更改并设置dirty? 这是我对 C++ 的抱怨之一:IMO 应该有一个 operator[]= 就像在 Ruby 中一样。 const 函数仅在调用它的对象是const 或没有非常量重载时才被调用。 【参考方案1】:

操作符按标准调用。

要了解原因:(除了极少数例外)C++ 中的所有(子)表达式的计算结果完全相同,而与封闭的(完整)表达式无关。

这意味着list[0] 的调用在int a = list[0]list[0] = 11 中完全相同。因此,当为list[0] 选择重载时,周围的表达式将被完全忽略。唯一需要考虑的是list 的类型和0 的类型。由于list 是非常量并且提供了完美匹配的非常量重载,因此选择非常量重载。

因此,您的问题是,当您在非常量对象上调用方法时,无法知道该方法是否会进一步用于读取或写入。

谢天谢地,这个问题有几个解决方案:

返回一个代理

返回一个代理对象,而不是返回对元素的引用。仅在尝试通过该代理对象写入时设置脏位。同样的策略可以扩展到使用迭代器。

提供一个常量视图

提供类似std::span:

std::span<const T> as_const_view() const;

这将隐式启用您想要的迭代器。缺点是用户需要注意你的类的这个怪癖,并避免调用operator[] 进行读取访问。

不要提供这些类型的重载

坚持getset,避免混淆。

【讨论】:

感谢您的解释。有没有办法使用 get/set 接口与 STL sort/nth_element 一起使用最少的脏位集?基本上是代理方法? @user1456982 要么将operator[] begin end 与代理类型一起使用,要么提供一个常量视图。【参考方案2】:

块基本上是从磁盘加载的缓存数据块(可以有 2-3 个这样的 LRU 块)。

假设您使用的是操作系统:操作系统已经为您完成了所有这些工作。您已经支付了进行 LRU 替换、缓冲区空间管理等变体的成本 - 它已经完成。所以你不应该这样做再次 - 有什么意义?如果有的话,为了帮助进行预取,您应该告诉操作系统您预计接下来要使用的文件位置 - 如果您由于列表的使用方式而获得此信息。除此之外,如果您认为出于数据完整性原因需要,只需将文件映射到内存并将其强制同步到磁盘。在 POSIX/Linux 平台上,您可以使用 msync,还有 a Windows equivalent。

调用的 operator[] 总是第一个设置脏位的。这不是我想要的。

根据 C++ 规范调用正确的运算符,因此即使您不知道自己确实需要它,实际上您也确实“想要”它。如果您稍微思考一下 const 方法重载解析的用途,您会意识到以任何其他方式执行此操作都会不可挽回地破坏语言。

如果有的话,您不应该设置脏位,直到该块中的某些内容实际发生变化,即该块应该提供 getter 和 setter 方法来更改其内容。在您现有的方法中,另一种方法是在单独映射的内存页面中为块分配内存,从磁盘加载块,然后将页面设置为写保护。真正的写访问将触发信号(在 Unix 上)或 Windows 上的类似机制,然后您可以取消保护页面,设置其脏位并恢复程序。

但是,一旦您对这些块来自的文件进行内存映射,这完全没有意义:操作系统会为您生成所有脏位!

【讨论】:

我只是使用文件来使问题更容易理解。实际的存储是另一回事。并且操作系统层不能参与。

以上是关于实现 operator[] / iterator 来处理性能问题的方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

返回多于 T& 和 std::pair 的 iterator::operator* 的标准接口

为什么没有operator + for std :: list iterators?

GeekBand_STL_Iterator

错误:'operator<<' 不匹配(操作数类型为'std::ostream' aka'std::basic_ostream<char>' 和'std::_List_iter

为自定义数组实现迭代器

STL deque