From 30e3a2516ff74569c5155a104920579c92fea46c Mon Sep 17 00:00:00 2001 From: Marcel Greter Date: Tue, 12 Nov 2019 09:57:14 +0100 Subject: [PATCH] Add custom memory allocator --- Makefile.conf | 3 +- docs/README.md | 28 ++- docs/allocator.md | 108 +++++++++++ docs/compatibility-plan.md | 48 ----- docs/developing.md | 11 ++ script/ci-build-plugin | 6 +- src/MurmurHash2.hpp | 91 ++++++++++ src/ast_def_macros.hpp | 4 +- src/ast_fwd_decl.hpp | 3 +- src/context.cpp | 16 +- src/error_handling.cpp | 2 +- src/error_handling.hpp | 4 +- src/eval.cpp | 2 +- src/file.cpp | 4 +- src/file.hpp | 4 +- src/fn_strings.cpp | 18 +- src/memory.hpp | 12 ++ src/memory/allocator.cpp | 47 +++++ src/memory/allocator.hpp | 141 +++++++++++++++ src/memory/config.hpp | 20 ++ src/memory/memory_pool.hpp | 181 +++++++++++++++++++ src/memory/{SharedPtr.cpp => shared_ptr.cpp} | 2 +- src/memory/{SharedPtr.hpp => shared_ptr.hpp} | 10 + src/operation.hpp | 2 +- src/parser.cpp | 10 +- src/sass.hpp | 18 +- src/settings.hpp | 19 ++ test/Makefile | 6 +- test/test_shared_ptr.cpp | 15 +- test/test_util_string.cpp | 30 +-- win/libsass.targets | 11 +- win/libsass.vcxproj.filters | 25 ++- 32 files changed, 774 insertions(+), 127 deletions(-) create mode 100644 docs/allocator.md delete mode 100644 docs/compatibility-plan.md create mode 100644 docs/developing.md create mode 100644 src/MurmurHash2.hpp create mode 100644 src/memory.hpp create mode 100644 src/memory/allocator.cpp create mode 100644 src/memory/allocator.hpp create mode 100644 src/memory/config.hpp create mode 100644 src/memory/memory_pool.hpp rename src/memory/{SharedPtr.cpp => shared_ptr.cpp} (96%) rename src/memory/{SharedPtr.hpp => shared_ptr.hpp} (97%) create mode 100644 src/settings.hpp diff --git a/Makefile.conf b/Makefile.conf index 2798301301..4bc0360e09 100644 --- a/Makefile.conf +++ b/Makefile.conf @@ -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 diff --git a/docs/README.md b/docs/README.md index c0d4491e2c..421393e9dc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,15 +1,23 @@ -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 @@ -17,4 +25,10 @@ Or make your own! |-------------------|----------------------------------|-----------------------------| | 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) diff --git a/docs/allocator.md b/docs/allocator.md new file mode 100644 index 0000000000..c4d0093a5f --- /dev/null +++ b/docs/allocator.md @@ -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. \ No newline at end of file diff --git a/docs/compatibility-plan.md b/docs/compatibility-plan.md deleted file mode 100644 index 3ece109a9b..0000000000 --- a/docs/compatibility-plan.md +++ /dev/null @@ -1,48 +0,0 @@ -This document is to serve as a living, changing plan for getting LibSass caught up with Ruby Sass. - -_Note: an "s" preceding a version number is specifying a Ruby Sass version. Without an s, it's a version of LibSass._ - -# Goal -**Our goal is to reach full s3.4 compatibility as soon as possible. LibSass version 3.4 will behave just like Ruby Sass 3.4** - -I highlight the goal, because there are some things that are *not* currently priorities. To be clear, they WILL be priorities, but they are not at the moment: - -* Performance Improvements -* Extensibility - -The overriding goal is correctness. - -## Verifying Correctness -LibSass uses the spec for its testing. The spec was originally based off s3.2 tests. Many things have changed in Ruby Sass since then and some of the tests need to be updated and changed in order to get them to match both LibSass and Ruby Sass. - -Until this project is complete, the spec will be primarily a place to test LibSass. By the time LibSass reaches 3.4, it is our goal that sass-spec will be fully usable as an official testing source for ALL implementations of Sass. - -## Version Naming -Until LibSass reaches parity with Ruby Sass, we will be aggressively bumping versions, and LibSass 3.4 will be the peer to Ruby Sass 3.4 in every way. - -# Release Plan - -## 3.0 -The goal of 3.0 is to introduce some of the most demanded features for LibSass. That is, we are focusing on issues and features that have kept adoption down. This is a mongrel release wrt which version of Sass it's targeting. It's often a mixture of 3.2 / 3.3 / 3.4 behaviours. This is not ideal, but it's favourable to not existing. Targeting 3.4 strictly during this release would mean we never actually release. - -# 3.1 -The goal of 3.1 is to update all the passing specs to agree with 3.4. This will not be a complete representation of s3.4 (aka, there will me missing features), but the goal is to change existing features and implemented features to match 3.4 behaviour. - -By the end of this, the sass-spec must pass against 3.4. - -Major issues: -* Variable Scoping -* Color Handling -* Precision - -# 3.2 -This version will focus on edge case fixes. There are a LOT of edge cases in the _todo_ tests and this is the release where we hunt those down like dogs (not that we want to hurt dogs, it's just a figure of speech in English). - -# 3.3 -Dress rehearsal. When we are 99% sure that we've fixed the main issues keeping us from saying we are compliant in s3.4 behaviour. - -# 3.4 -Compass Compatibility. We need to be able to work with Compass and all the other libraries out there. At this point, we are calling LibSass "mature" - -# Beyond 3.4 -Obviously, there is matching Sass 3.5 behaviour. But, beyond that, we'll want to focus on performance, stability, and error handling. These can always be improved upon and are the life's work of an open source project. We'll have to work closely with Sass in the future. diff --git a/docs/developing.md b/docs/developing.md new file mode 100644 index 0000000000..cb94253c8c --- /dev/null +++ b/docs/developing.md @@ -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) diff --git a/script/ci-build-plugin b/script/ci-build-plugin index dc2faf4d20..533a3f512e 100755 --- a/script/ci-build-plugin +++ b/script/ci-build-plugin @@ -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 diff --git a/src/MurmurHash2.hpp b/src/MurmurHash2.hpp new file mode 100644 index 0000000000..ab9b1634c6 --- /dev/null +++ b/src/MurmurHash2.hpp @@ -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 + +#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_ + diff --git a/src/ast_def_macros.hpp b/src/ast_def_macros.hpp index a89d994aa6..8a889cd9cc 100644 --- a/src/ast_def_macros.hpp +++ b/src/ast_def_macros.hpp @@ -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; \ } \ @@ -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(); \ diff --git a/src/ast_fwd_decl.hpp b/src/ast_fwd_decl.hpp index 7e1d2f8c08..c4f4229201 100644 --- a/src/ast_fwd_decl.hpp +++ b/src/ast_fwd_decl.hpp @@ -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. diff --git a/src/context.cpp b/src/context.cpp index 8152aee6b8..306ce04afa 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -557,7 +557,9 @@ namespace Sass { } // abort early if no content could be loaded (various reasons) - if (!contents) throw std::runtime_error("File to read not found or unreadable: " + input_path); + if (!contents) throw std::runtime_error( + "File to read not found or unreadable: " + + std::string(input_path.c_str())); // store entry path entry_path = abs_path; @@ -673,8 +675,8 @@ namespace Sass { sass::string Context::format_embedded_source_map() { sass::string map = emitter.render_srcmap(*this); - std::istringstream is( map ); - std::ostringstream buffer; + sass::istream is( map.c_str() ); + sass::sstream buffer; base64::encoder E; E.encode(is, buffer); sass::string url = "data:application/json;base64," + buffer.str(); @@ -729,11 +731,11 @@ namespace Sass { void register_overload_stub(Context& ctx, sass::string name, Env* env) { Definition* stub = SASS_MEMORY_NEW(Definition, - SourceSpan("[built-in function]"), - 0, + SourceSpan{ "[built-in function]" }, + nullptr, name, - {}, - 0, + Parameters_Obj{}, + nullptr, true); (*env)[name + "[f]"] = stub; } diff --git a/src/error_handling.cpp b/src/error_handling.cpp index 3ef0fee1f1..52f0c37744 100644 --- a/src/error_handling.cpp +++ b/src/error_handling.cpp @@ -14,7 +14,7 @@ namespace Sass { namespace Exception { Base::Base(SourceSpan pstate, sass::string msg, Backtraces traces) - : std::runtime_error(msg), msg(msg), + : std::runtime_error(msg.c_str()), msg(msg), prefix("Error"), pstate(pstate), traces(traces) { } diff --git a/src/error_handling.hpp b/src/error_handling.hpp index d067976f42..b185fac3ef 100644 --- a/src/error_handling.hpp +++ b/src/error_handling.hpp @@ -23,7 +23,7 @@ namespace Sass { const sass::string def_msg = "Invalid sass detected"; const sass::string def_op_msg = "Undefined operation"; const sass::string def_op_null_msg = "Invalid null operation"; - const sass::string def_nesting_limit = "Code too deeply neested"; + const sass::string def_nesting_limit = "Code too deeply nested"; class Base : public std::runtime_error { protected: @@ -152,7 +152,7 @@ namespace Sass { sass::string msg; public: OperationError(sass::string msg = def_op_msg) - : std::runtime_error(msg), msg(msg) + : std::runtime_error(msg.c_str()), msg(msg) {}; public: virtual const char* errtype() const { return "Error"; } diff --git a/src/eval.cpp b/src/eval.cpp index 94fbca628c..351abbb47e 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -952,7 +952,7 @@ namespace Sass { { if (traces.size() > Constants::MaxCallStack) { // XXX: this is never hit via spec tests - std::ostringstream stm; + sass::ostream stm; stm << "Stack depth exceeded max of " << Constants::MaxCallStack; error(stm.str(), c->pstate(), traces); } diff --git a/src/file.cpp b/src/file.cpp index 7a6dde341c..9772c6b634 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -32,7 +32,7 @@ # ifdef _MSC_VER # include -inline static Sass::sass::string wstring_to_string(const std::wstring& wstr) +inline static std::string wstring_to_string(const std::wstring& wstr) { std::wstring_convert, wchar_t> wchar_converter; return wchar_converter.to_bytes(wstr); @@ -68,7 +68,7 @@ namespace Sass { wchar_t wd[wd_len]; wchar_t* pwd = _wgetcwd(wd, wd_len); if (pwd == NULL) throw Exception::OperationError("cwd gone missing"); - sass::string cwd = wstring_to_string(pwd); + sass::string cwd = wstring_to_string(pwd).c_str(); //convert backslashes to forward slashes replace(cwd.begin(), cwd.end(), '\\', '/'); #endif diff --git a/src/file.hpp b/src/file.hpp index b949569b55..78b5244408 100644 --- a/src/file.hpp +++ b/src/file.hpp @@ -114,10 +114,8 @@ namespace Sass { namespace File { - static sass::vector defaultExtensions = { ".scss", ".sass", ".css" }; - sass::vector resolve_includes(const sass::string& root, const sass::string& file, - const sass::vector& exts = defaultExtensions); + const sass::vector& exts = { ".scss", ".sass", ".css" }); } diff --git a/src/fn_strings.cpp b/src/fn_strings.cpp index 03e6c44835..58bf60733d 100644 --- a/src/fn_strings.cpp +++ b/src/fn_strings.cpp @@ -100,7 +100,11 @@ namespace Sass { sass::string ins = i->value(); double index = ARGVAL("$index"); if (index != (int)index) { - error("$index: " + std::to_string(index) + " is not an int", pstate, traces); + sass::ostream strm; + strm << "$index: "; + strm << std::to_string(index); + strm << " is not an int"; + error(strm.str(), pstate, traces); } size_t len = UTF_8::code_point_count(str, 0, str.size()); @@ -168,7 +172,11 @@ namespace Sass { double end_at = ARGVAL("$end-at"); if (start_at != (int)start_at) { - error("$start-at: " + std::to_string(start_at) + " is not an int", pstate, traces); + sass::ostream strm; + strm << "$start-at: "; + strm << std::to_string(start_at); + strm << " is not an int"; + error(strm.str(), pstate, traces); } String_Quoted* ss = Cast(s); @@ -182,7 +190,11 @@ namespace Sass { } if (end_at != (int)end_at) { - error("$end-at: " + std::to_string(end_at) + " is not an int", pstate, traces); + sass::ostream strm; + strm << "$end-at: "; + strm << std::to_string(end_at); + strm << " is not an int"; + error(strm.str(), pstate, traces); } if (end_at == 0 || (end_at + size) < 0) { diff --git a/src/memory.hpp b/src/memory.hpp new file mode 100644 index 0000000000..14a5541b6d --- /dev/null +++ b/src/memory.hpp @@ -0,0 +1,12 @@ +#ifndef SASS_MEMORY_H +#define SASS_MEMORY_H + +#include "settings.hpp" + +// Include memory headers +#include "memory/config.hpp" +#include "memory/allocator.hpp" +#include "memory/shared_ptr.hpp" +#include "memory/memory_pool.hpp" + +#endif diff --git a/src/memory/allocator.cpp b/src/memory/allocator.cpp new file mode 100644 index 0000000000..bbbbde5e6e --- /dev/null +++ b/src/memory/allocator.cpp @@ -0,0 +1,47 @@ +#include "../sass.hpp" +#include "../memory.hpp" + +#if defined (_MSC_VER) // Visual studio +#define thread_local __declspec( thread ) +#elif defined (__GCC__) // GCC +#define thread_local __thread +#endif + +namespace Sass { + +#ifdef SASS_CUSTOM_ALLOCATOR + + // Only use PODs for thread_local + // Objects get unpredictable init order + static thread_local MemoryPool* pool; + static thread_local size_t allocations; + + void* allocateMem(size_t size) + { + if (pool == nullptr) { + pool = new MemoryPool(); + } + allocations++; + return pool->allocate(size); + } + + void deallocateMem(void* ptr, size_t size) + { + + // It seems thread_local variable might be discharged!? + // But the destructors of e.g. static strings is still + // called, although their memory was discharged too. + // Fine with me as long as address sanitizer is happy. + if (pool == nullptr || allocations == 0) { return; } + + pool->deallocate(ptr); + if (--allocations == 0) { + delete pool; + pool = nullptr; + } + + } + +#endif + +} diff --git a/src/memory/allocator.hpp b/src/memory/allocator.hpp new file mode 100644 index 0000000000..8454c14a73 --- /dev/null +++ b/src/memory/allocator.hpp @@ -0,0 +1,141 @@ +#ifndef SASS_ALLOCATOR_H +#define SASS_ALLOCATOR_H + +#include "config.hpp" +#include "../settings.hpp" +#include "../MurmurHash2.hpp" + +#include +#include +#include +#include +#include + +namespace Sass { + +#ifndef SASS_CUSTOM_ALLOCATOR + + template using Allocator = std::allocator; + inline void* allocateMem(size_t size) { return malloc(size); } + inline void deallocateMem(void* ptr, size_t = 1) { free(ptr); } + +#else + + void* allocateMem(size_t size); + + void deallocateMem(void* ptr, size_t size = 1); + + template + class Allocator + { + public: + + // Allocator traits + typedef T type; + typedef type value_type; + typedef value_type* pointer; + typedef value_type const* const_pointer; + typedef value_type& reference; + typedef value_type const& const_reference; + typedef std::size_t size_type; + typedef std::ptrdiff_t difference_type; + + template + struct rebind + { + typedef Allocator other; + }; + + // Constructor + Allocator(void) {} + + // Copy Constructor + template + Allocator(Allocator const&) + {} + + // allocate but don't initialize count of elements of type T + pointer allocate(size_type count, const_pointer /* hint */ = 0) + { + return (pointer)(Sass::allocateMem(count * sizeof(T))); + } + + // deallocate storage ptr of deleted elements + void deallocate(pointer ptr, size_type count) + { + Sass::deallocateMem(ptr, count); + } + + // return maximum number of elements that can be allocated + size_type max_size() const throw() + { + return std::numeric_limits::max() / sizeof(T); + } + + // Address of object + type* address(type& obj) const { return &obj; } + type const* address(type const& obj) const { return &obj; } + + // Construct object + void construct(type* ptr, type const& ref) const + { + // In-place copy construct + new(ptr) type(ref); + } + + // Destroy object + void destroy(type* ptr) const + { + // Call destructor + ptr->~type(); + } + + }; + + template + bool operator==(Allocator const& left, + Allocator const& right) + { + return true; + } + + template + bool operator!=(Allocator const& left, + Allocator const& right) + { + return !(left == right); + } + +#endif + + namespace sass { + template using vector = std::vector>; + using string = std::basic_string, Sass::Allocator>; + using sstream = std::basic_stringstream, Sass::Allocator>; + using ostream = std::basic_ostringstream, Sass::Allocator>; + using istream = std::basic_istringstream, Sass::Allocator>; + } + +} + +#ifdef SASS_CUSTOM_ALLOCATOR + +namespace std { + // Only GCC seems to need this specialization!? + template <> struct hash { + public: + inline size_t operator()( + const Sass::sass::string& name) const + { + return MurmurHash2( + (void*)name.c_str(), + (int)name.size(), + 0x73617373); + } + }; +} + + +#endif + +#endif diff --git a/src/memory/config.hpp b/src/memory/config.hpp new file mode 100644 index 0000000000..9d666b2e6b --- /dev/null +++ b/src/memory/config.hpp @@ -0,0 +1,20 @@ +#ifndef SASS_MEMORY_CONFIG_H +#define SASS_MEMORY_CONFIG_H + +// Define memory alignment requirements +#define SASS_MEM_ALIGN sizeof(unsigned int) + +// Minimal alignment for memory fragments. Must be a multiple +// of `SASS_MEM_ALIGN` and should not be too big (maybe 1 or 2) +#define SassAllocatorHeadSize sizeof(unsigned int) + +// The number of bytes we use for our book-keeping before every +// memory fragment. Needed to know to which bucket we belongs on +// deallocations, or if it should go directly to the `free` call. +#define SassAllocatorBookSize sizeof(unsigned int) + +// Bytes reserve for book-keeping on the arenas +// Currently unused and for later optimization +#define SassAllocatorArenaHeadSize 0 + +#endif \ No newline at end of file diff --git a/src/memory/memory_pool.hpp b/src/memory/memory_pool.hpp new file mode 100644 index 0000000000..bd534c4ce4 --- /dev/null +++ b/src/memory/memory_pool.hpp @@ -0,0 +1,181 @@ +#ifndef SASS_MEMORY_POOL_H +#define SASS_MEMORY_POOL_H + +#include "../sass.hpp" +#include "../memory.hpp" +#include + +namespace Sass { + + // SIMPLE MEMORY-POOL ALLOCATOR WITH FREE-LIST ON TOP + + // This is a memory pool allocator with a free list on top. + // We only allocate memory arenas from the system in specific + // sizes (`SassAllocatorArenaSize`). Users claim memory slices + // of certain sizes from the pool. If the allocation is too big + // to fit into our buckets, we use regular malloc/free instead. + + // When the systems starts, we allocate the first arena and then + // start to give out addresses to memory slices. During that + // we steadily increase `offset` until the current arena is full. + // Once that happens we allocate a new arena and continue. + // https://en.wikipedia.org/wiki/Memory_pool + + // Fragments that get deallocated are not really freed, we put + // them on our free-list. For every bucket we have a pointer to + // the first item for reuse. That item itself holds a pointer to + // the previously free item (regular free-list implementation). + // https://en.wikipedia.org/wiki/Free_list + + // On allocation calls we first check if there is any suitable + // item on the free-list. If there is we pop it from the stack + // and return it to the caller. Otherwise we have to take out + // a new slice from the current `arena` and increase `offset`. + + // Note that this is not thread safe. This is on purpose as we + // want to use the memory pool in a thread local usage. In order + // to get this thread safe you need to only allocate one pool + // per thread. This can be achieved by using thread local PODs. + // Simply create a pool on the first allocation and dispose + // it once all allocations have been returned. E.g. by using: + // static thread_local size_t allocations; + // static thread_local MemoryPool* pool; + + class MemoryPool { + + // Current arena we fill up + char* arena; + + // Position into the arena + size_t offset = std::string::npos; + + // A list of full arenas + std::vector arenas; + + // One pointer for every bucket (zero init) + void* freeList[SassAllocatorBuckets] = {}; + + // Increase the address until it sits on a + // memory aligned address (maybe use `aligned`). + inline static size_t alignMemAddr(size_t addr) { + return (addr + SASS_MEM_ALIGN - 1) & ~(SASS_MEM_ALIGN - 1); + } + + public: + + // Default ctor + MemoryPool() : + // Wait for first allocation + arena(nullptr), + // Set to maximum value in order to + // make an allocation on the first run + offset(std::string::npos) + { + } + + // Destructor + ~MemoryPool() { + // Delete full arenas + for (auto area : arenas) { + free(area); + } + // Delete current arena + free(arena); + + } + + // Allocate a slice of the memory pool + void* allocate(size_t size) + { + + // Increase size so its memory is aligned + size = alignMemAddr( + // Make sure we have enough space for us to + // create the pointer to the free list later + std::max(sizeof(void*), size) + // and the size needed for our book-keeping + + SassAllocatorBookSize); + + // Size must be multiple of alignment + // So we can derive bucket index from it + size_t bucket = size / SASS_MEM_ALIGN; + + // Everything bigger is allocated via malloc + // Malloc is optimized for exactly this case + if (bucket >= SassAllocatorBuckets) { + char* buffer = (char*)malloc(size); + if (buffer == nullptr) { + throw std::bad_alloc(); + } + // Mark it for deallocation via free + ((unsigned int*)buffer)[0] = UINT_MAX; + // Return pointer after our book-keeping space + return (void*)(buffer + SassAllocatorBookSize); + } + // Use custom allocator + else { + // Get item from free list + void*& free = freeList[bucket]; + // Do we have a free item? + if (free != nullptr) { + // Copy pointer to return + void* ptr = free; + // Update free list pointer + free = ((void**)ptr)[0]; + // Return popped item + return ptr; + } + } + + // Make sure we have enough space in the arena + if (!arena || offset > SassAllocatorArenaSize - size) { + if (arena) arenas.emplace_back(arena); + arena = (char*)malloc(SassAllocatorArenaSize); + if (arena == nullptr) throw std::bad_alloc(); + offset = SassAllocatorArenaHeadSize; + } + + // Get pointer into the arena + char* buffer = arena + offset; + // Consume object size + offset += size; + + // Set the bucket index for this slice + ((unsigned int*)buffer)[0] = (unsigned int)bucket; + + // Return pointer after our book-keeping space + return (void*)(buffer + SassAllocatorBookSize); + + } + // EO allocate + + void deallocate(void* ptr) + { + + // Rewind buffer from pointer + char* buffer = (char*)ptr - + SassAllocatorBookSize; + + // Get the bucket index stored in the header + unsigned int bucket = ((unsigned int*)buffer)[0]; + + // Check allocation method + if (bucket != UINT_MAX) { + // Let memory point to previous item in free list + ((void**)ptr)[0] = freeList[bucket]; + // Free list now points to our memory + freeList[bucket] = (void*)ptr; + } + else { + // Release memory + free(buffer); + } + + } + // EO deallocate + + }; + +} + +#endif diff --git a/src/memory/SharedPtr.cpp b/src/memory/shared_ptr.cpp similarity index 96% rename from src/memory/SharedPtr.cpp rename to src/memory/shared_ptr.cpp index 79901199bc..faa5796fab 100644 --- a/src/memory/SharedPtr.cpp +++ b/src/memory/shared_ptr.cpp @@ -2,7 +2,7 @@ #include #include -#include "SharedPtr.hpp" +#include "shared_ptr.hpp" #include "../ast_fwd_decl.hpp" #ifdef DEBUG_SHARED_PTR diff --git a/src/memory/SharedPtr.hpp b/src/memory/shared_ptr.hpp similarity index 97% rename from src/memory/SharedPtr.hpp rename to src/memory/shared_ptr.hpp index c447c99074..39d263159e 100644 --- a/src/memory/SharedPtr.hpp +++ b/src/memory/shared_ptr.hpp @@ -4,6 +4,7 @@ #include "sass/base.h" #include "../sass.hpp" +#include "allocator.hpp" #include #include #include @@ -16,6 +17,7 @@ namespace Sass { + // Forward declaration class SharedPtr; /////////////////////////////////////////////////////////////////////////////// @@ -90,6 +92,14 @@ namespace Sass { static void setTaint(bool val) { taint = val; } + inline void* operator new(size_t nbytes) { + return allocateMem(nbytes); + } + + inline void operator delete(void* ptr) { + return deallocateMem(ptr); + } + virtual sass::string to_string() const = 0; protected: friend class SharedPtr; diff --git a/src/operation.hpp b/src/operation.hpp index b9f589f1a7..fafd2c73be 100644 --- a/src/operation.hpp +++ b/src/operation.hpp @@ -213,7 +213,7 @@ namespace Sass { template inline T fallback(U x) { throw std::runtime_error( - sass::string(typeid(*this).name()) + ": CRTP not implemented for " + typeid(x).name()); + std::string(typeid(*this).name()) + ": CRTP not implemented for " + typeid(x).name()); } }; diff --git a/src/parser.cpp b/src/parser.cpp index 27e534a46e..4847d81f62 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -646,7 +646,7 @@ namespace Sass { // normalize underscores to hyphens sass::string name(Util::normalize_underscores(lexed)); // create the initial mixin call object - Mixin_Call_Obj call = SASS_MEMORY_NEW(Mixin_Call, pstate, name, {}, {}, {}); + Mixin_Call_Obj call = SASS_MEMORY_NEW(Mixin_Call, pstate, name, Arguments_Obj{}); // parse mandatory arguments call->arguments(parse_arguments()); // parse using and optional block parameters @@ -827,11 +827,11 @@ namespace Sass { if (!lex_css< attribute_name >()) error("invalid attribute name in attribute selector"); sass::string name(lexed); if (lex_css< re_attr_sensitive_close >()) { - return SASS_MEMORY_NEW(AttributeSelector, p, name, "", {}, {}); + return SASS_MEMORY_NEW(AttributeSelector, p, name, "", String_Obj{}); } else if (lex_css< re_attr_insensitive_close >()) { char modifier = lexed.begin[0]; - return SASS_MEMORY_NEW(AttributeSelector, p, name, "", {}, modifier); + return SASS_MEMORY_NEW(AttributeSelector, p, name, "", String_Obj{}, modifier); } if (!lex_css< alternatives< exact_match, class_match, dash_match, prefix_match, suffix_match, substring_match > >()) { @@ -2110,7 +2110,7 @@ namespace Sass { stack.push_back(Scope::Control); bool root = block_stack.back()->is_root(); // create the initial while call object - WhileRuleObj call = SASS_MEMORY_NEW(WhileRule, pstate, {}, {}); + WhileRuleObj call = SASS_MEMORY_NEW(WhileRule, pstate, ExpressionObj{}, Block_Obj{}); // parse mandatory predicate ExpressionObj predicate = parse_list(); List_Obj l = Cast(predicate); @@ -2272,7 +2272,7 @@ namespace Sass { { if (lex < identifier_schema >()) { String_Obj ss = parse_identifier_schema(); - return SASS_MEMORY_NEW(Media_Query_Expression, pstate, ss, {}, true); + return SASS_MEMORY_NEW(Media_Query_Expression, pstate, ss, ExpressionObj{}, true); } if (!lex_css< exactly<'('> >()) { error("media query expression must begin with '('"); diff --git a/src/sass.hpp b/src/sass.hpp index 9e5f6e12df..f202bf79bf 100644 --- a/src/sass.hpp +++ b/src/sass.hpp @@ -11,7 +11,7 @@ #pragma warning(disable : 4005) #endif -// aplies to MSVC and MinGW +// applies to MSVC and MinGW #ifdef _WIN32 // we do not want the ERROR macro # ifndef NOGDI @@ -48,23 +48,19 @@ #endif -// include C-API header +// Include C-API header #include "sass/base.h" +// Include allocator +#include "memory.hpp" + // For C++ helper #include #include -// output behaviour +// output behavior namespace Sass { - namespace sass { - // Create some aliases - using string = std::string; - using sstream = std::stringstream; - template using vector = std::vector; - } - // create some C++ aliases for the most used options const static Sass_Output_Style NESTED = SASS_STYLE_NESTED; const static Sass_Output_Style COMPACT = SASS_STYLE_COMPACT; @@ -81,7 +77,7 @@ namespace Sass { } -// input behaviours +// input behaviors enum Sass_Input_Style { SASS_CONTEXT_NULL, SASS_CONTEXT_FILE, diff --git a/src/settings.hpp b/src/settings.hpp new file mode 100644 index 0000000000..febebe8a94 --- /dev/null +++ b/src/settings.hpp @@ -0,0 +1,19 @@ +#ifndef SASS_SETTINGS_H +#define SASS_SETTINGS_H + +// Global compile time settings should go here + +// When enabled we use our custom memory pool allocator +// With intense workloads this can double the performance +// Max memory usage mostly only grows by a slight amount +#define SASS_CUSTOM_ALLOCATOR + +// 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) + +#endif diff --git a/test/Makefile b/test/Makefile index f990a43d66..0f9aa30bcf 100644 --- a/test/Makefile +++ b/test/Makefile @@ -12,11 +12,11 @@ test_util_string: build/test_util_string build: @mkdir build -build/test_shared_ptr: test_shared_ptr.cpp ../src/memory/SharedPtr.cpp | build - $(CXX) $(CXXFLAGS) -o build/test_shared_ptr test_shared_ptr.cpp ../src/memory/SharedPtr.cpp +build/test_shared_ptr: test_shared_ptr.cpp ../src/memory/shared_ptr.cpp | build + $(CXX) $(CXXFLAGS) ../src/memory/allocator.cpp ../src/memory/shared_ptr.cpp -o build/test_shared_ptr test_shared_ptr.cpp build/test_util_string: test_util_string.cpp ../src/util_string.cpp | build - $(CXX) $(CXXFLAGS) -o build/test_util_string test_util_string.cpp ../src/util_string.cpp + $(CXX) $(CXXFLAGS) ../src/memory/allocator.cpp ../src/util_string.cpp -o build/test_util_string test_util_string.cpp clean: | build rm -rf build diff --git a/test/test_shared_ptr.cpp b/test/test_shared_ptr.cpp index 2656088274..7355c1abd8 100644 --- a/test/test_shared_ptr.cpp +++ b/test/test_shared_ptr.cpp @@ -1,4 +1,5 @@ -#include "../src/memory/SharedPtr.hpp" +#include "../src/memory/allocator.hpp" +#include "../src/memory/shared_ptr.hpp" #include #include @@ -15,8 +16,8 @@ class TestObj : public Sass::SharedObj { public: TestObj(bool *destroyed) : destroyed_(destroyed) {} ~TestObj() { *destroyed_ = true; } - std::string to_string() const { - std::ostringstream result; + Sass::sass::string to_string() const { + Sass::sass::ostream result; result << "refcount=" << refcount << " destroyed=" << *destroyed_; return result.str(); } @@ -29,7 +30,7 @@ using SharedTestObj = Sass::SharedImpl; bool TestOneSharedPtr() { bool destroyed = false; { - SharedTestObj a = new TestObj(&destroyed); + SharedTestObj a = SASS_MEMORY_NEW(TestObj, &destroyed); } ASSERT(destroyed); return true; @@ -38,7 +39,7 @@ bool TestOneSharedPtr() { bool TestTwoSharedPtrs() { bool destroyed = false; { - SharedTestObj a = new TestObj(&destroyed); + SharedTestObj a = SASS_MEMORY_NEW(TestObj, &destroyed); { SharedTestObj b = a; } @@ -51,7 +52,7 @@ bool TestTwoSharedPtrs() { bool TestSelfAssignment() { bool destroyed = false; { - SharedTestObj a = new TestObj(&destroyed); + SharedTestObj a = SASS_MEMORY_NEW(TestObj, &destroyed); a = a; ASSERT(!destroyed); } @@ -137,7 +138,7 @@ bool TestDetachNull() { class EmptyTestObj : public Sass::SharedObj { public: - std::string to_string() const { return ""; } + Sass::sass::string to_string() const { return ""; } }; bool TestComparisonWithSharedPtr() { diff --git a/test/test_util_string.cpp b/test/test_util_string.cpp index b0df8af2cb..a44e6e1670 100644 --- a/test/test_util_string.cpp +++ b/test/test_util_string.cpp @@ -7,8 +7,8 @@ namespace { -std::string escape_string(const std::string& str) { - std::string out; +Sass::sass::string escape_string(const Sass::sass::string& str) { + Sass::sass::string out; out.reserve(str.size()); for (char c : str) { switch (c) { @@ -50,57 +50,57 @@ std::string escape_string(const std::string& str) { } \ bool TestNormalizeNewlinesNoNewline() { - std::string input = "a"; - std::string normalized = Sass::Util::normalize_newlines(input); + Sass::sass::string input = "a"; + Sass::sass::string normalized = Sass::Util::normalize_newlines(input); ASSERT_STR_EQ(input, normalized); return true; } bool TestNormalizeNewlinesLF() { - std::string input = "a\nb"; - std::string normalized = Sass::Util::normalize_newlines(input); + Sass::sass::string input = "a\nb"; + Sass::sass::string normalized = Sass::Util::normalize_newlines(input); ASSERT_STR_EQ(input, normalized); return true; } bool TestNormalizeNewlinesCR() { - std::string normalized = Sass::Util::normalize_newlines("a\rb"); + Sass::sass::string normalized = Sass::Util::normalize_newlines("a\rb"); ASSERT_STR_EQ("a\nb", normalized); return true; } bool TestNormalizeNewlinesCRLF() { - std::string normalized = Sass::Util::normalize_newlines("a\r\nb\r\n"); + Sass::sass::string normalized = Sass::Util::normalize_newlines("a\r\nb\r\n"); ASSERT_STR_EQ("a\nb\n", normalized); return true; } bool TestNormalizeNewlinesFF() { - std::string normalized = Sass::Util::normalize_newlines("a\fb\f"); + Sass::sass::string normalized = Sass::Util::normalize_newlines("a\fb\f"); ASSERT_STR_EQ("a\nb\n", normalized); return true; } bool TestNormalizeNewlinesMixed() { - std::string normalized = Sass::Util::normalize_newlines("a\fb\nc\rd\r\ne\ff"); + Sass::sass::string normalized = Sass::Util::normalize_newlines("a\fb\nc\rd\r\ne\ff"); ASSERT_STR_EQ("a\nb\nc\nd\ne\nf", normalized); return true; } bool TestNormalizeUnderscores() { - std::string normalized = Sass::Util::normalize_underscores("a_b_c"); + Sass::sass::string normalized = Sass::Util::normalize_underscores("a_b_c"); ASSERT_STR_EQ("a-b-c", normalized); return true; } bool TestNormalizeDecimalsLeadingZero() { - std::string normalized = Sass::Util::normalize_decimals("0.5"); + Sass::sass::string normalized = Sass::Util::normalize_decimals("0.5"); ASSERT_STR_EQ("0.5", normalized); return true; } bool TestNormalizeDecimalsNoLeadingZero() { - std::string normalized = Sass::Util::normalize_decimals(".5"); + Sass::sass::string normalized = Sass::Util::normalize_decimals(".5"); ASSERT_STR_EQ("0.5", normalized); return true; } @@ -143,14 +143,14 @@ bool TestUnvendor() { } bool Test_ascii_str_to_lower() { - std::string str = "A B"; + Sass::sass::string str = "A B"; Sass::Util::ascii_str_tolower(&str); ASSERT_STR_EQ("a b", str); return true; } bool Test_ascii_str_to_upper() { - std::string str = "a b"; + Sass::sass::string str = "a b"; Sass::Util::ascii_str_toupper(&str); ASSERT_STR_EQ("A B", str); return true; diff --git a/win/libsass.targets b/win/libsass.targets index ba83de4c1e..0a781af46c 100644 --- a/win/libsass.targets +++ b/win/libsass.targets @@ -14,8 +14,14 @@ + + + + + + + - @@ -85,7 +91,8 @@ - + + diff --git a/win/libsass.vcxproj.filters b/win/libsass.vcxproj.filters index 2f86668692..6897471e7e 100644 --- a/win/libsass.vcxproj.filters +++ b/win/libsass.vcxproj.filters @@ -66,7 +66,25 @@ Headers - + + Headers + + + Headers + + + Headers + + + Headers + + + Headers + + + Headers + + Headers @@ -260,7 +278,10 @@ Sources - + + Sources + + Sources