Skip to content

Commit b10866c

Browse files
authored
Merge pull request #3038 from mgreter/feature/custom-allocator
Add custom memory allocator (opt-in only so far) Might be activated by default on libsass version 3.7.
2 parents a3e4df0 + 5aae535 commit b10866c

40 files changed

+803
-158
lines changed

Makefile.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ SOURCES = \
6565
to_value.cpp \
6666
source_map.cpp \
6767
error_handling.cpp \
68-
memory/SharedPtr.cpp \
68+
memory/allocator.cpp \
69+
memory/shared_ptr.cpp \
6970
utf8_string.cpp \
7071
base64vlq.cpp
7172

docs/README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
1-
Welcome to the LibSass documentation!
1+
## LibSass documentation
22

3-
## First Off
4-
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!
3+
LibSass is just a library. To run the code locally (i.e. to compile your
4+
stylesheets), you need an implementer. SassC is an implementer written in C.
5+
There are a number of other implementations of LibSass - for example NodeJS.
6+
We encourage you to write your own port - the whole point of LibSass is that
7+
we want to bring Sass to many other languages!
58

6-
We're working hard on moving to full parity with Ruby Sass... learn more at the [The-LibSass-Compatibility-Plan](compatibility-plan.md)!
9+
## LibSass road-map
10+
11+
Since ruby-sass was retired in 2019 in favor of dart-sass, we slowly move
12+
toward full compatibility with the latest Sass specifications, although
13+
features like the module `@use` system may take a bit longer to add.
714

815
### Implementing LibSass
916

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

1422
### Contributing to LibSass
1523

1624
| Issue Tracker | Issue Triage | Community Guidelines |
1725
|-------------------|----------------------------------|-----------------------------|
1826
| 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! |
1927

28+
### Building LibSass
29+
2030
Please refer to the steps on [Building LibSass](build.md)
31+
32+
### Developing LibSass
33+
34+
Please refer to [Developing LibSass](developing.md)

docs/allocator.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
## Custom memory allocator
2+
3+
LibSass comes with a custom memory allocator to improve performance.
4+
First included in LibSass 3.6 and currently disabled by default.
5+
Needs to be enabled by defining `SASS_CUSTOM_ALLOCATOR`.
6+
7+
### Overview
8+
9+
The allocator is a pool/arena allocator with a free-list on top. The
10+
memory usage pattern of LibSass fits this implementation very well.
11+
Every compilation tends to accumulate memory and only releasing some
12+
items from time to time, but the overall memory consumption will mostly
13+
grow until the compilation is finished. This helps us to keep the
14+
implementation as simple as possible, since we don't need to release
15+
much memory back to the system and can re-use it instead.
16+
17+
### Arenas
18+
19+
Each arena is allocated in a (compile time) fixed size. Every allocation
20+
request is served from the current arena. We basically slice up the
21+
arena into different sized chunks. Arenas are never returned to the
22+
system until the whole compilation is finished.
23+
24+
### Slices
25+
26+
A memory slice is a part of an arena. Once the system requests a sized
27+
memory chunk we check the current arena if there is enough space to
28+
hold it. If not a new arena is allocated. Then we return a pointer
29+
into that arena and mark the space as being used. Each slice also
30+
has a header which is invisible to the requester as it lies before
31+
the pointer address that we returned. This is used for book-keeping.
32+
33+
### Free-lists (or buckets)
34+
35+
Once a memory slice is returned to the allocator it will not be released.
36+
It will instead be put on the free list. We keep a fixed number of free lists,
37+
one for every possible chunk size. Since chunk sizes are memory aligned, we
38+
can get the free-list index (aka `bucket`) very quickly (`size/alignment`).
39+
For further readings see https://en.wikipedia.org/wiki/Free_list.
40+
41+
### Chunk-sizes
42+
43+
Since arenas are of fixed size we need to make sure that only small
44+
enough chunks get served from it. This also helps to keep implementation
45+
simple, as we can statically declare some structures for book-keeping.
46+
Allocations that are too big to be tracked on a free-list will be patched
47+
directly to malloc and free. This is the case when the bucket index would
48+
be bigger than `SassAllocatorBuckets`.
49+
50+
### Thread-safety
51+
52+
This implementation is not thread-safe by design. Making it thread-safe
53+
would certainly be possible, but it would come at a (performance) price.
54+
Also it is not needed given the memory usage pattern of LibSass. Instead
55+
we should make sure that memory pools are local to each thread.
56+
57+
### Implementation obstacles
58+
59+
Since memory allocation is a core part of C++ itself, we get into various
60+
difficult territories. This has specially proven true in regard of static
61+
variable initialization and destruction order. E.g when we have a static
62+
string with custom allocator. It might be that it is initialized before
63+
the thread local memory pool. On the other hand it's also possible that
64+
the memory pool is destroyed before another static string wants to give
65+
back its memory to the pool. I tried hard to work around those issues.
66+
Mainly by only using thead local POD (plain old data) objects.
67+
68+
See https://isocpp.org/wiki/faq/ctors#static-init-order
69+
70+
### Performance gains
71+
72+
My tests indicate that the custom allocator brings around 15% performance
73+
enhancement for complex cases (I used the bolt-bench to measure it). Once
74+
more get optimized, the custom allocator can bring up to 30% improvement.
75+
This comes at a small cost of a few percent of overall memory usage. This
76+
can be tweaked, but the sweet spot for now seems to be:
77+
78+
```c
79+
// How many buckets should we have for the free-list
80+
// Determines when allocations go directly to malloc/free
81+
// For maximum size of managed items multiply by alignment
82+
#define SassAllocatorBuckets 512
83+
84+
// The size of the memory pool arenas in bytes.
85+
#define SassAllocatorArenaSize (1024 * 256)
86+
```
87+
88+
These can be found in `settings.hpp`.
89+
90+
### Memory overhead
91+
92+
Both settings `SassAllocatorBuckets` and `SassAllocatorArenaSize` need
93+
to be set in relation to each other. Assuming the memory alignment on
94+
the platform is 8 bytes, the maximum chunk size that can be handled
95+
is 4KB (512*8B). If the arena size is too close to this value, you
96+
may leave a lot of RAM unused. Once an arena can't fullfil the current
97+
request, it is put away and a new one is allocated. We don't keep track
98+
of unused space in previous arenas, as it bloats the code and costs
99+
precious cpu time. By setting the values carefully we can avoid the cost
100+
and still provide reasonable memory overhead. In the worst scenario we
101+
loose around 1.5% for the default settings (4K of 256K).
102+
103+
### Further improvements
104+
105+
It is worth to check if we can re-use the space of old arenas somehow without
106+
scarifying to much performance. Additionally we could check free-lists of
107+
bigger chunks sizes to satisfy an allocation request. But both would need
108+
to be checked for performance impact and their actual gain.

docs/compatibility-plan.md

Lines changed: 0 additions & 48 deletions
This file was deleted.

docs/developing.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Developing LibSass
2+
3+
So far this is only a loose collection of developer relevant docs:
4+
5+
- [Building LibSass](build.md)
6+
- [Profiling LibSass](dev-profiling.md)
7+
- [C-API documentation](api-doc.md)
8+
- [LibSass and Unicode](unicode.md)
9+
- [SourceMap internals](source-map-internals.md)
10+
- [Custom memory allocator](allocator.md)
11+
- [Smart pointer implementation](dev-ast-memory.md)

script/ci-build-plugin

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ fi
3434

3535
mkdir -p plugins
3636
if [ ! -d plugins/libsass-${PLUGIN} ] ; then
37-
git clone https://github.com/mgreter/libsass-${PLUGIN} plugins/libsass-${PLUGIN}
37+
if [ "$PLUGIN" == "tests" ]; then
38+
git clone https://github.com/mgreter/libsass-${PLUGIN} plugins/libsass-${PLUGIN} --branch master
39+
else
40+
git clone https://github.com/mgreter/libsass-${PLUGIN} plugins/libsass-${PLUGIN}
41+
fi
3842
fi
3943
if [ ! -d plugins/libsass-${PLUGIN}/build ] ; then
4044
mkdir plugins/libsass-${PLUGIN}/build

src/MurmurHash2.hpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//-----------------------------------------------------------------------------
2+
// MurmurHash2 was written by Austin Appleby, and is placed in the public
3+
// domain. The author hereby disclaims copyright to this source code.
4+
//-----------------------------------------------------------------------------
5+
// LibSass only needs MurmurHash2, so we made this header only
6+
//-----------------------------------------------------------------------------
7+
8+
#ifndef _MURMURHASH2_H_
9+
#define _MURMURHASH2_H_
10+
11+
//-----------------------------------------------------------------------------
12+
// Platform-specific functions and macros
13+
14+
// Microsoft Visual Studio
15+
16+
#if defined(_MSC_VER) && (_MSC_VER < 1600)
17+
18+
typedef unsigned char uint8_t;
19+
typedef unsigned int uint32_t;
20+
typedef unsigned __int64 uint64_t;
21+
22+
// Other compilers
23+
24+
#else // defined(_MSC_VER)
25+
26+
#include <stdint.h>
27+
28+
#endif // !defined(_MSC_VER)
29+
30+
//-----------------------------------------------------------------------------
31+
32+
inline uint32_t MurmurHash2 ( const void * key, int len, uint32_t seed )
33+
{
34+
// 'm' and 'r' are mixing constants generated offline.
35+
// They're not really 'magic', they just happen to work well.
36+
37+
const uint32_t m = 0x5bd1e995;
38+
const int r = 24;
39+
40+
// Initialize the hash to a 'random' value
41+
42+
uint32_t h = seed ^ len;
43+
44+
// Mix 4 bytes at a time into the hash
45+
46+
const unsigned char * data = (const unsigned char *)key;
47+
48+
while(len >= 4)
49+
{
50+
uint32_t k = *(uint32_t*)data;
51+
52+
k *= m;
53+
k ^= k >> r;
54+
k *= m;
55+
56+
h *= m;
57+
h ^= k;
58+
59+
data += 4;
60+
len -= 4;
61+
}
62+
63+
// Handle the last few bytes of the input array
64+
65+
switch(len)
66+
{
67+
case 3:
68+
h ^= data[2] << 16;
69+
/* fall through */
70+
case 2:
71+
h ^= data[1] << 8;
72+
/* fall through */
73+
case 1:
74+
h ^= data[0];
75+
h *= m;
76+
};
77+
78+
// Do a few final mixes of the hash to ensure the last few
79+
// bytes are well-incorporated.
80+
81+
h ^= h >> 13;
82+
h *= m;
83+
h ^= h >> 15;
84+
85+
return h;
86+
}
87+
88+
//-----------------------------------------------------------------------------
89+
90+
#endif // _MURMURHASH2_H_
91+

src/ast_def_macros.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public: \
113113

114114
#define IMPLEMENT_AST_OPERATORS(klass) \
115115
klass* klass::copy(sass::string file, size_t line) const { \
116-
klass* cpy = new klass(this); \
116+
klass* cpy = SASS_MEMORY_NEW(klass, this); \
117117
cpy->trace(file, line); \
118118
return cpy; \
119119
} \
@@ -127,7 +127,7 @@ public: \
127127

128128
#define IMPLEMENT_AST_OPERATORS(klass) \
129129
klass* klass::copy() const { \
130-
return new klass(this); \
130+
return SASS_MEMORY_NEW(klass, this); \
131131
} \
132132
klass* klass::clone() const { \
133133
klass* cpy = copy(); \

src/ast_fwd_decl.hpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
// sass.hpp must go before all system headers to get the
55
// __EXTENSIONS__ fix on Solaris.
66
#include "sass.hpp"
7-
7+
#include "memory.hpp"
88
#include "sass/functions.h"
9-
#include "memory/SharedPtr.hpp"
109

1110
/////////////////////////////////////////////
1211
// Forward declarations for the AST visitors.

src/backtrace.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace Sass {
44

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

7-
sass::sstream ss;
7+
sass::ostream ss;
88
sass::string cwd(File::get_cwd());
99

1010
bool first = true;

0 commit comments

Comments
 (0)