实现 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[]
传统上用于 getting 和 setting。也就是说,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[]
进行读取访问。
不要提供这些类型的重载
坚持get
和set
,避免混淆。
【讨论】:
感谢您的解释。有没有办法使用 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?
错误:'operator<<' 不匹配(操作数类型为'std::ostream' aka'std::basic_ostream<char>' 和'std::_List_iter