Using std::shared_ptr<T> to implement copy-on-write objects in Octave

For some time, I’ve been replacing reference counted objects in Octave that use a custom reference count with std::shared_ptr<T> objects instead. Examples of this include

changeset:   7733ca1db419
date:        Wed Apr 28 08:18:43 2021 -0400
summary:     use shared_ptr to manage memory for mutex object

changeset:   bffdb54e78da
date:        Tue Apr 27 16:12:41 2021 -0400
summary:     use shared_ptr to manage memory for sparse chol and qr classes

changeset:   ed90a4d75f6d
date:        Tue Apr 27 13:16:01 2021 -0400
summary:     use shared_ptr to manage data for hook_function class

changeset:   c850a9cd28f6
date:        Wed Mar 10 23:05:17 2021 -0500
summary:     use std::shared_ptr and m_ prefix for vertex_data and opengl_texture classes

changeset:   9a3deb17b4ea
date:        Sat Apr 25 13:17:11 2020 -0400
summary:     use shared_ptr for stack frames in call stack and for accesss and static links

changeset:   8600f5ea1ec1
date:        Thu Nov 21 19:23:20 2019 -0600
summary:     use std::shared_ptr to manage stream rep

changeset:   da163456abb3
date:        Thu Nov 21 17:08:59 2019 -0500
summary:     use std::shared_ptr to manage graphics_toolkit rep

changeset:   7a31b25e3252
date:        Thu Oct 10 13:33:33 2019 -0400
summary:     use shared_ptr for storing classdef and statement_list objects in parser

changeset:   1bc237447e56
date:        Wed Oct 16 10:56:10 2019 -0400
summary:     use shared_ptr to manage base_reader object

changeset:   d67f369b3074
date:        Thu Jul 18 09:58:41 2019 -0400
summary:     use shared_ptr to manage octave_link_events object

changeset:   8bcfddad15ec
date:        Mon Nov 27 01:12:05 2017 -0500
summary:     use shared_ptr to manage symbol_scope objects

changeset:   5abd4d7cbd36
date:        Mon Nov 27 10:48:20 2017 -0500
summary:     use shared_ptr to manage fcn_info object

changeset:   0dd6c909baa2
date:        Fri Nov 17 09:12:11 2017 -0500
summary:     use shared_ptr and weak_ptr to manage symbol_record object

changeset:   4bca68f0d8d5
date:        Wed Nov 15 14:52:07 2017 -0500
summary:     use shared_ptr to manage url_transfer object

changeset:   cf15cb87bad9
date:        Wed Nov 15 14:38:31 2017 -0500
summary:     use shared_ptr to manage thread_manager object

changeset:   f8c263f961c1
date:        Wed Nov 15 14:21:52 2017 -0500
summary:     use shared_ptr to manage graphics_object and graphics_event objects

So far, I don’t think any of the objects that have been converted to use std::shared_ptr<T> have had copy-on-write (COW) semantics. So if there are multiple references to one of these objects and a non-const method is called through one of the shared pointers, then no copy is made. All references to the (only) actual object will also observe any changes. That’s the behavior that we want for things like graphics objects and symbol table scope information.

If I understand correctly, copying std::shared_ptr<T> is a thread-safe operation, but access to the underlying object requires synchronization. In Octave, I think we get part of this right by copying the shared pointer objects from one thread to another (rather than passing a reference, for example). But it’s not clear to me that we always use the appropriate mutex locking when accessing the data that these shared pointer objects reference. Doing some code review of any objects that use std::shared_ptr<T> and are passed from one thread to another would probably be worth doing.

Most of the remaining objects that use reference counts also have copy-on-write semantics. For example, Array<T> and octave_value objects in Octave work this way. This is why, in code like

A = magic (5);
B = A;
B(3,3) = pi;

A and B are different after the assignment to B(3,3) but a copy of A is not made until B is modified.

All of Octave’s COW objects have something like the following method to create a copy prior to any modification in non-const methods:

void object::make_unique (void)
{
  if (m_rep->m_count > 1)
    {
      object_rep *new_rep = new object_rep (...);

      if (--m_rep->m_count == 0)
        delete m_rep;

      m_rep = new_rep;
    }
}

To implement the same kind of COW operation with std::shared_ptr<T>, you might think to use something like

void object::make_unique (void)
{
  if (! m_rep.unique ())
    m_rep = std::shared_ptr<object_rep> (new object_rep (...));
}

That looks great! It’s simpler and the deletion of the old m_rep object will happen automatically. But std::shared_ptr<T>::unique is deprecated in C++20 and (see std::shared_ptr<T>::use_count - cppreference.com):

In multithreaded environment, the value returned by use_count is approximate (typical implementations use a memory_order_relaxed load)

and

In multithreaded environment, [use_count == 1] does not imply that the object is safe to modify because accesses to the managed object by former shared owners may not have completed, and because new shared owners may be introduced concurrently, such as by std::weak_ptr::lock.

The (since C++11 and deprecated in C++20) atomic load/store functions (std::atomic_...<std::shared_ptr> - cppreference.com) and (since C++20) partial specialization for std::atomic<std::shared_ptr<T>> (std::atomic(std::shared_ptr) - cppreference.com) are apparently intended to help with these problems but I’m still not sure precisely how.

Digging a little more, you can find a number of discussions about this topic and some example code. For example, GitHub - HadrienG2/copy-on-write-ptr: A C++ smart pointer with copy-on-write semantics provides three different implementations of a smart pointer for COW objects.

Any pointers and/or help with understanding what we should do here to improve Octave would be much appreciated.

Even if we don’t switch to using std::shared_ptr<T> in some way for the COW objects in Octave, this topic still raises the question as to whether our current method is safe for multiple threads. Is using an atomic object for the m_count sufficient to make functions like object::make_unique above thread safe? Are there other instances where we are accessing shared resources managed with std::shared_ptr<T> (symbol scopes, graphics objects, etc.) in multiple threads without the necessary synchronization?

1 Like

Your analysis seems correct. I would have done exactly what you did and looked for someone else to have already solved this. I guess we could create our own smart pointer with COW semantics as a class and use it until C++ develops a library alternative, at which point the implementation in our class becomes a trivial call to the library. Until then, we would have something that works regardless of the changes and deprecations in the standard library.

I’m pretty sure that we’ve never had a full code review for whether our code is thread safe. It is 1) a good idea, 2) a lot of work, 3) and I won’t be much use since I don’t regularly work with threads and don’t know what to check for.