Skip to content

Commit

Permalink
Merge pull request #3038 from mgreter/feature/custom-allocator
Browse files Browse the repository at this point in the history
Add custom memory allocator (opt-in only so far)

Might be activated by default on libsass version 3.7.
  • Loading branch information
mgreter authored Nov 15, 2019
2 parents a3e4df0 + 5aae535 commit b10866c
Show file tree
Hide file tree
Showing 40 changed files with 803 additions and 158 deletions.
3 changes: 2 additions & 1 deletion Makefile.conf
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ SOURCES = \
to_value.cpp \
source_map.cpp \
error_handling.cpp \
memory/SharedPtr.cpp \
memory/allocator.cpp \
memory/shared_ptr.cpp \
utf8_string.cpp \
base64vlq.cpp

Expand Down
28 changes: 21 additions & 7 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
Welcome to the LibSass documentation!
## LibSass documentation

## First Off
LibSass is just a library. To run the code locally (i.e. to compile your stylesheets), you need an implementer. SassC (get it?) is an implementer written in C. There are a number of other implementations of LibSass - for example Node. We encourage you to write your own port - the whole point of LibSass is that we want to bring Sass to many other languages, not just Ruby!
LibSass is just a library. To run the code locally (i.e. to compile your
stylesheets), you need an implementer. SassC is an implementer written in C.
There are a number of other implementations of LibSass - for example NodeJS.
We encourage you to write your own port - the whole point of LibSass is that
we want to bring Sass to many other languages!

We're working hard on moving to full parity with Ruby Sass... learn more at the [The-LibSass-Compatibility-Plan](compatibility-plan.md)!
## LibSass road-map

Since ruby-sass was retired in 2019 in favor of dart-sass, we slowly move
toward full compatibility with the latest Sass specifications, although
features like the module `@use` system may take a bit longer to add.

### Implementing LibSass

If you're interested in implementing LibSass in your own project see the [API Documentation](api-doc.md) which now includes implementing
your own [Sass functions](api-function.md). You may wish to [look at other implementations](implementations.md) for your language of choice.
Or make your own!
If you're interested in implementing LibSass in your own project see the
[API Documentation](api-doc.md) which now includes implementing your own
[Sass functions](api-function.md). You may wish to [look at other
implementations](implementations.md) for your language of choice.

### Contributing to LibSass

| Issue Tracker | Issue Triage | Community Guidelines |
|-------------------|----------------------------------|-----------------------------|
| We're always needing help, so check out our issue tracker, help some people out, and read our article on [Contributing](contributing.md)! It's got all the details on what to do! | To help understand the process of triaging bugs, have a look at our [Issue-Triage](triage.md) document. | Oh, and don't forget we always follow [Sass Community Guidelines](https://sass-lang.com/community-guidelines). Be nice and everyone else will be nice too! |

### Building LibSass

Please refer to the steps on [Building LibSass](build.md)

### Developing LibSass

Please refer to [Developing LibSass](developing.md)
108 changes: 108 additions & 0 deletions docs/allocator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
## Custom memory allocator

LibSass comes with a custom memory allocator to improve performance.
First included in LibSass 3.6 and currently disabled by default.
Needs to be enabled by defining `SASS_CUSTOM_ALLOCATOR`.

### Overview

The allocator is a pool/arena allocator with a free-list on top. The
memory usage pattern of LibSass fits this implementation very well.
Every compilation tends to accumulate memory and only releasing some
items from time to time, but the overall memory consumption will mostly
grow until the compilation is finished. This helps us to keep the
implementation as simple as possible, since we don't need to release
much memory back to the system and can re-use it instead.

### Arenas

Each arena is allocated in a (compile time) fixed size. Every allocation
request is served from the current arena. We basically slice up the
arena into different sized chunks. Arenas are never returned to the
system until the whole compilation is finished.

### Slices

A memory slice is a part of an arena. Once the system requests a sized
memory chunk we check the current arena if there is enough space to
hold it. If not a new arena is allocated. Then we return a pointer
into that arena and mark the space as being used. Each slice also
has a header which is invisible to the requester as it lies before
the pointer address that we returned. This is used for book-keeping.

### Free-lists (or buckets)

Once a memory slice is returned to the allocator it will not be released.
It will instead be put on the free list. We keep a fixed number of free lists,
one for every possible chunk size. Since chunk sizes are memory aligned, we
can get the free-list index (aka `bucket`) very quickly (`size/alignment`).
For further readings see https://en.wikipedia.org/wiki/Free_list.

### Chunk-sizes

Since arenas are of fixed size we need to make sure that only small
enough chunks get served from it. This also helps to keep implementation
simple, as we can statically declare some structures for book-keeping.
Allocations that are too big to be tracked on a free-list will be patched
directly to malloc and free. This is the case when the bucket index would
be bigger than `SassAllocatorBuckets`.

### Thread-safety

This implementation is not thread-safe by design. Making it thread-safe
would certainly be possible, but it would come at a (performance) price.
Also it is not needed given the memory usage pattern of LibSass. Instead
we should make sure that memory pools are local to each thread.

### Implementation obstacles

Since memory allocation is a core part of C++ itself, we get into various
difficult territories. This has specially proven true in regard of static
variable initialization and destruction order. E.g when we have a static
string with custom allocator. It might be that it is initialized before
the thread local memory pool. On the other hand it's also possible that
the memory pool is destroyed before another static string wants to give
back its memory to the pool. I tried hard to work around those issues.
Mainly by only using thead local POD (plain old data) objects.

See https://isocpp.org/wiki/faq/ctors#static-init-order

### Performance gains

My tests indicate that the custom allocator brings around 15% performance
enhancement for complex cases (I used the bolt-bench to measure it). Once
more get optimized, the custom allocator can bring up to 30% improvement.
This comes at a small cost of a few percent of overall memory usage. This
can be tweaked, but the sweet spot for now seems to be:

```c
// How many buckets should we have for the free-list
// Determines when allocations go directly to malloc/free
// For maximum size of managed items multiply by alignment
#define SassAllocatorBuckets 512

// The size of the memory pool arenas in bytes.
#define SassAllocatorArenaSize (1024 * 256)
```
These can be found in `settings.hpp`.
### Memory overhead
Both settings `SassAllocatorBuckets` and `SassAllocatorArenaSize` need
to be set in relation to each other. Assuming the memory alignment on
the platform is 8 bytes, the maximum chunk size that can be handled
is 4KB (512*8B). If the arena size is too close to this value, you
may leave a lot of RAM unused. Once an arena can't fullfil the current
request, it is put away and a new one is allocated. We don't keep track
of unused space in previous arenas, as it bloats the code and costs
precious cpu time. By setting the values carefully we can avoid the cost
and still provide reasonable memory overhead. In the worst scenario we
loose around 1.5% for the default settings (4K of 256K).
### Further improvements
It is worth to check if we can re-use the space of old arenas somehow without
scarifying to much performance. Additionally we could check free-lists of
bigger chunks sizes to satisfy an allocation request. But both would need
to be checked for performance impact and their actual gain.
48 changes: 0 additions & 48 deletions docs/compatibility-plan.md

This file was deleted.

11 changes: 11 additions & 0 deletions docs/developing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Developing LibSass

So far this is only a loose collection of developer relevant docs:

- [Building LibSass](build.md)
- [Profiling LibSass](dev-profiling.md)
- [C-API documentation](api-doc.md)
- [LibSass and Unicode](unicode.md)
- [SourceMap internals](source-map-internals.md)
- [Custom memory allocator](allocator.md)
- [Smart pointer implementation](dev-ast-memory.md)
6 changes: 5 additions & 1 deletion script/ci-build-plugin
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ fi

mkdir -p plugins
if [ ! -d plugins/libsass-${PLUGIN} ] ; then
git clone https://github.com/mgreter/libsass-${PLUGIN} plugins/libsass-${PLUGIN}
if [ "$PLUGIN" == "tests" ]; then
git clone https://github.com/mgreter/libsass-${PLUGIN} plugins/libsass-${PLUGIN} --branch master
else
git clone https://github.com/mgreter/libsass-${PLUGIN} plugins/libsass-${PLUGIN}
fi
fi
if [ ! -d plugins/libsass-${PLUGIN}/build ] ; then
mkdir plugins/libsass-${PLUGIN}/build
Expand Down
91 changes: 91 additions & 0 deletions src/MurmurHash2.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//-----------------------------------------------------------------------------
// MurmurHash2 was written by Austin Appleby, and is placed in the public
// domain. The author hereby disclaims copyright to this source code.
//-----------------------------------------------------------------------------
// LibSass only needs MurmurHash2, so we made this header only
//-----------------------------------------------------------------------------

#ifndef _MURMURHASH2_H_
#define _MURMURHASH2_H_

//-----------------------------------------------------------------------------
// Platform-specific functions and macros

// Microsoft Visual Studio

#if defined(_MSC_VER) && (_MSC_VER < 1600)

typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef unsigned __int64 uint64_t;

// Other compilers

#else // defined(_MSC_VER)

#include <stdint.h>

#endif // !defined(_MSC_VER)

//-----------------------------------------------------------------------------

inline uint32_t MurmurHash2 ( const void * key, int len, uint32_t seed )
{
// 'm' and 'r' are mixing constants generated offline.
// They're not really 'magic', they just happen to work well.

const uint32_t m = 0x5bd1e995;
const int r = 24;

// Initialize the hash to a 'random' value

uint32_t h = seed ^ len;

// Mix 4 bytes at a time into the hash

const unsigned char * data = (const unsigned char *)key;

while(len >= 4)
{
uint32_t k = *(uint32_t*)data;

k *= m;
k ^= k >> r;
k *= m;

h *= m;
h ^= k;

data += 4;
len -= 4;
}

// Handle the last few bytes of the input array

switch(len)
{
case 3:
h ^= data[2] << 16;
/* fall through */
case 2:
h ^= data[1] << 8;
/* fall through */
case 1:
h ^= data[0];
h *= m;
};

// Do a few final mixes of the hash to ensure the last few
// bytes are well-incorporated.

h ^= h >> 13;
h *= m;
h ^= h >> 15;

return h;
}

//-----------------------------------------------------------------------------

#endif // _MURMURHASH2_H_

4 changes: 2 additions & 2 deletions src/ast_def_macros.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public: \

#define IMPLEMENT_AST_OPERATORS(klass) \
klass* klass::copy(sass::string file, size_t line) const { \
klass* cpy = new klass(this); \
klass* cpy = SASS_MEMORY_NEW(klass, this); \
cpy->trace(file, line); \
return cpy; \
} \
Expand All @@ -127,7 +127,7 @@ public: \

#define IMPLEMENT_AST_OPERATORS(klass) \
klass* klass::copy() const { \
return new klass(this); \
return SASS_MEMORY_NEW(klass, this); \
} \
klass* klass::clone() const { \
klass* cpy = copy(); \
Expand Down
3 changes: 1 addition & 2 deletions src/ast_fwd_decl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
// sass.hpp must go before all system headers to get the
// __EXTENSIONS__ fix on Solaris.
#include "sass.hpp"

#include "memory.hpp"
#include "sass/functions.h"
#include "memory/SharedPtr.hpp"

/////////////////////////////////////////////
// Forward declarations for the AST visitors.
Expand Down
2 changes: 1 addition & 1 deletion src/backtrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Sass {

const sass::string traces_to_string(Backtraces traces, sass::string indent) {

sass::sstream ss;
sass::ostream ss;
sass::string cwd(File::get_cwd());

bool first = true;
Expand Down
Loading

0 comments on commit b10866c

Please sign in to comment.