Skip to content

doodspav/patomic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

patomic

This library provides portable access to lock-free atomic operations at runtime through a unified C90 interface.

Transactional operations are also provided in a similar manner, except there is no guarantee that a transaction ever succeeds. It should also be noted that platform support for transactional operations is very rare.

The goal of this library is to provide the foundation for an atomics library in Python, however there is no reason it could not be used in any other language.

Table of Contents

Versioning

This library follows Semantic Versioning.

Improvements to non-code files such as documentation or pipeline will not result in a version bump.

Building

CMake

This project uses CMake as its build system.

To build and run the tests on any platform:

mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DBUILD_TESTING=ON ..
cmake --build . --config Release
ctest -C Release

Note:

Building the tests requires that CMake can find GoogleTest.

You may need to pass -DGTest_ROOT=<path/to/gtest> as an extra option when configuring CMake.

Without CMake

The following header files are generated by CMake and need to be provided manually:

  • <patomic/api/export.h>
  • <patomic/api/version.h>
  • (private) <patomic/_config.h>

The following include paths are set:

  • /include/
  • (private) /src/include/

With these set, it should be trivial to compile and link all the .c files in /src.

This will not compile any tests.

Platform Requirements

Building this library requires a C90 standards compliant compiler.

Building in freestanding is not supported out of the box. The following changes need to be made:

  • replace <string.h> which is included in multiple files for memcpy and memcmp
  • replace most files in /src/stdlib/ (their corresponding headers should be ok)

Building the tests for this library requires C++14, and whatever other requirements GoogleTest has.

Note:

Although this library will compile with just C90, it will likely report that no operations are supported.

To have supported operations, you will need to compile this library with a compiler that supports non-C90 features such as <stdatomic.h>.

This will not affect the public interface, which will always remain 100% C90.

Configuration

By default, CMake will generate a _config.h file which is included in /src/include/patomic/config.h, which provides macros defining all available non-standard C90 functionality.

It is possible to override almost all of these macros if you know that some functionality is available but hasn't been detected, or if you want to disable some functionality from being used.

Usage

Usage of this library is split into four stages:

  • creating a patomic object
  • checking that the desired functionality is supported
  • (atomic only) check if the alignment requirements are met
  • perform the operation
  • (transaction only) check if the transaction succeeded

Before reading on, it is recommended that you look through the public header files (found in /include/).

They are thoroughly documented, and give a helpful overview for what is available.

Create patomic Object

The types patomic_t, patomic_explicit_t, and patomic_transaction_t have an .ops member which contains (possibly null) function pointers for all operations.

The first two types also have an .align member which contains alignment requirements for the non-null operations.

Objects of these types can be obtained through the patomic_create family of APIs, such as:

patomic_t
patomic_create(
    size_t byte_width,
    patomic_memory_order_t order,
    unsigned int options,
    unsigned int kinds,
    unsigned long ids
);

Implementation options, kinds, and ids can be found in /include/patomic/api/options.h and /include/patomic/api/ids.h.

However, the defaults patomic_option_NONE, patomic_kinds_ALL, and patomic_ids_ALL should be sufficient for most use cases.

Feature Check

The .ops member is a collection of function pointers denoting operations. These may be null if the operation is not supported, so they must be checked before use.

The feature_check_{any, all, leaf} family of APIs can be used for this, such as:

/* setup */
patomic_t pa = patomic_create(...);

/* check that fp_add and fp_sub are supported */
unsigned int kinds = patomic_opkind_ADD | patomic_opkind_SUB;
kinds = patomic_feature_check_leaf(
    &pa.ops,
    patomic_opcat_ARI_V,
    kinds
);

/* all bits corresponding to supported operations are unset */
assert(kinds == 0);

Enum values can be found in /include/patomic/api/feature_check.h.

Note:

All this does is provide an easy way to check if a pointer is null.

You are free to do this check manually, especially if you only care about one or two operations.

Alignment Check

Non-transaction atomic operations have alignment requirements on the atomic object for their operations.

These can be checked using the align_meets family of APIs, like so:

/* setup */
int a = 0;
patomic_t pa = patomic_create(...);

/* check that the standard alignment is met */
assert(patomic_align_meets_recommended(
    &a, pa.align
));

/* check that the (possibly less strict) hardware alignment is met */
assert(patomic_align_meets_minimum(
    &a, pa.align, sizeof(a)
));

Note:

These functions may rely on implementation-defined behaviour. If that is undesirable, you should use your own versions of these functions.

There are no alignment requirements for any non-atomic objects used in atomic operations, even if the object being used has an alignment greater than 1.

Perform Operation

This is as simple as calling the function pointer corresponding to the operation.

/* setup */
patomic_t pa = patomic_create(...);
int obj = 0;
const int arg = 5;
int ret = -1;

/* perform operation (checks omitted) */
pa.ops.arithmetic_ops.fp_fetch_add(&obj, &arg, &ret);
assert(obj == 5);
assert(ret == 0);

Transaction Result

Transactions allow you to perform more complex operations, as well as perform them on larger objects.

They come with the drawback that the operation might never succeed; this always needs to be checked after calling the operation.

Transactions also require hardware support, making it very unlikely that they are supported on most platforms.

/* setup */
patomic_transaction_t pa = patomic_create_transaction(...);
int obj = 2;
int expected = 0;
const int desired = 5;

/* configure */
patomic_transaction_config_wfb_t config = {0};
config.width = sizeof(obj);
config.attempts = 1000;
config.fallback_attempts = 1000;

/* perform operation (checks omitted) */
patomic_transaction_result_wfb_t result = {0};
int ok = pa.ops.xchg_ops.fp_cmpxchg_weak(
    &obj, &expected, &desired, config, &result
);

With a standard atomic operation, there are two possible outcomes here:

  • ok == 0: the operation failed, and expected == 2 (the value of obj)
  • ok == 1: the operation succeeded, and obj == 5 (the value of desired)

In this standard atomic case the outcome would be the first option (ok == 0) because obj != expected.

With a transaction, there is a third option:

  • ok == 0: the operation failed because every transaction attempt failed, and no value was modified

This only happens when neither the main transaction nor the fallback transaction succeeded, which can be checked with the result object:

/* check that transaction or fallback transaction succeeded */
assert(result.status == 0 || result.fallback_status == 0);

More information can be obtained from the status using the transaction_status family of APIs.

Note:

Not every operation has a fallback; most do not. The cmpxchg_weak operation is one of a few special cases.

Example

These three examples perform the same operation. One uses an atomic operation with an implicit memory order, one uses an atomic operation with an explicit memory order, and one uses an atomic transaction operation.

Implicit

/* create patomic object */
patomic_t pa = patomic_create(
    sizeof(int),
    patomic_SEQ_CST,
    patomic_option_NONE,
    patomic_kinds_ALL,
    patomic_ids_ALL
);
int obj;

/* feature check */
unsigned int kinds = patomic_opkind_ADD;
kinds = patomic_feature_check_leaf(
    &pa.ops,
    patomic_opcat_ARI_F,
    kinds
);
assert(kinds == 0);

/* alignment check */
const int align_ok = patomic_align_meets_recommended(
    &obj,
    pa.align
);
assert(align_ok);

/* perform operation */
obj = 1;
const int arg = 5;
int ret;
pa.ops.arithmetic_ops.fp_fetch_add(&obj, &arg, &ret);

/* check value */
assert(obj == 6);
assert(ret == 1);

Explicit

/* create patomic object */
patomic_explicit_t pa = patomic_create_explicit(
    sizeof(int),
    patomic_option_NONE,
    patomic_kinds_ALL,
    patomic_ids_ALL
);
int obj;

/* feature check */
unsigned int kinds = patomic_opkind_ADD;
kinds = patomic_feature_check_leaf_explicit(
    &pa.ops,
    patomic_opcat_ARI_F,
    kinds
);
assert(kinds == 0);

/* alignment check */
const int align_ok = patomic_align_meets_recommended(
    &obj,
    pa.align
);
assert(align_ok);

/* perform operation */
obj = 1;
const int arg = 5;
int ret;
pa.ops.arithmetic_ops.fp_fetch_add(&obj, &arg, patomic_SEQ_CST, &ret);

/* check value */
assert(obj == 6);
assert(ret == 1);

Transaction

/* create patomic object */
patomic_transaction_t pa = patomic_create_transaction(
    patomic_option_NONE,
    patomic_kinds_ALL,
    patomic_ids_ALL
);
int obj;

/* feature check */
unsigned int kinds = patomic_opkind_ADD;
kinds = patomic_feature_check_leaf_transaction(
    &pa.ops,
    patomic_opcat_ARI_F,
    kinds
);
assert(kinds == 0);

/* perform operation */
obj = 1;
const int arg = 5;
int ret;
patomic_transaction_config_t config = {0};
config.width = sizeof(obj);
config.attempts = 1000;
patomic_transaction_result_t result = {0};
pa.ops.arithmetic_ops.fp_fetch_add(&obj, &arg, &ret, config, &result);

/* check transaction */
assert(result.status == 0);

/* check value */
assert(obj == 6);
assert(ret == 1);

Traps and Pitfalls

The biggest pitfall is not reading the documentation. Here are some others:

1.

Non-transaction atomic operations all operate on unsigned integer objects.

This means that passing a signed integer object will only work on platforms where both signed and unsigned objects have the same representation.

2.

Where multiple integer types have the same width, non-transaction atomic operations for that width will use the highest ranking unsigned integer type which can be used for atomic operations.

This could cause issues on platforms where multiple integer types have the same width but different representations (e.g. one has trap bits).

3.

Atomic transaction operations all operate on bytes (unsigned char) rather than objects.

This could cause issues when trying to use them on integer types that have padding or trap bits.

4.

Atomic transaction operations might never succeed.

You should always have a non-transaction fallback, and should never assume that outputs have been initialized without checking the result status first.

Contributing

There is currently no framework for contributing changes.

Feel free to raise an issue if you would like to do so.

License

Copyright (c) doodspav.

SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception