From 025622f7ba344b1e47dd47c92ed380902ecbddfd Mon Sep 17 00:00:00 2001 From: Bent Bisballe Nyeng Date: Sun, 6 Aug 2023 18:18:31 +0200 Subject: A6: WIP (code is done) --- a6/Makefile | 29 +++++++--- a6/au_BentBisballeNyeng_A6.tex | 117 +++++++++++++++++++++++++++++-------- a6/custom.cc | 128 +++++++++++++++++++++++++++++++++++++++++ a6/generator_stack.h | 94 ++++++++++++++++++++++++++++++ a6/noalloc.cc | 119 ++++++++++++++++++++++++++------------ 5 files changed, 419 insertions(+), 68 deletions(-) create mode 100644 a6/custom.cc create mode 100644 a6/generator_stack.h diff --git a/a6/Makefile b/a6/Makefile index 61ea860..bc38dd3 100644 --- a/a6/Makefile +++ b/a6/Makefile @@ -1,15 +1,26 @@ +TEX_FLAGS=-halt-on-error -file-line-error -interaction=batchmode +TEX_NAME=au_BentBisballeNyeng_A6 +CXX_FLAGS=-g -O2 -fconcepts -fcoroutines -Wall -Werror -Wextra -Wconversion -std=c++20 + all: noalloc pdf noalloc: noalloc.cc Makefile - g++ -g -O0 -Wall -Werror -Wextra -Wconversion -std=c++20 $< -o $@ + g++ ${CXX_FLAGS} $< -o $@ + +stack_new: stack_new.cc Makefile + g++ ${CXX_FLAGS} $< -o $@ + +custom: custom.cc Makefile + g++ ${CXX_FLAGS} $< -o $@ + +pdf: ${TEX_NAME}.pdf -pdf: au_BentBisballeNyeng_A6.pdf -au_BentBisballeNyeng_A6.bbl: au_BentBisballeNyeng_A6.bcf - biber $< +${TEX_NAME}.bbl: ${TEX_NAME}.bcf + biber --onlylog $< -au_BentBisballeNyeng_A6.bcf: au_BentBisballeNyeng_A6.tex - xelatex -halt-on-error $< +${TEX_NAME}.bcf: ${TEX_NAME}.tex + xelatex --no-pdf --no-aux ${TEX_FLAGS} $< -au_BentBisballeNyeng_A6.pdf: au_BentBisballeNyeng_A6.tex au_BentBisballeNyeng_A6.bbl - xelatex -halt-on-error $< - xelatex -halt-on-error $< +${TEX_NAME}.pdf: ${TEX_NAME}.tex ${TEX_NAME}.bbl +# xelatex --no-pdf ${TEX_FLAGS} $< + xelatex ${TEX_FLAGS} $< diff --git a/a6/au_BentBisballeNyeng_A6.tex b/a6/au_BentBisballeNyeng_A6.tex index ad4cc0d..c6ad150 100644 --- a/a6/au_BentBisballeNyeng_A6.tex +++ b/a6/au_BentBisballeNyeng_A6.tex @@ -90,9 +90,11 @@ along with not supporting exceptions and run-time type information\cite{craig}. Working with the resulting small sub-set of the available components, -however, is not well suited for making contemporary C++ applications. +however, is not well suited for making contemporary C++ applications; +too many core components are missing so be able to really call it +contemporary. The ideal solution would be to find ways to be able to use all (or at -least most) features, but with a potential known set of restictions or +least more) features, but with a potential known set of restictions or limitations. \section{Method} @@ -100,28 +102,73 @@ In the following, 3 methods for managing memory allocations will be investigated, and their suitability for real-life applications be evaluated: \begin{itemize} -\item Using Custom Allocators for the STL components that supports it. -\item Overloading \texttt{new}/\texttt{delete} to use stack allocated - memory instead of the free-store for all allocation. \item Support from the compiler to fail compilation if an unintentional \texttt{new} or \texttt{delete} is being called, at least preventing accidental allocations. +\item Using Custom Allocators for the STL components that supports it. +\item Overloading \texttt{new}/\texttt{delete} to use stack allocated + memory instead of the free-store for all allocation. \end{itemize} -Each of the three will be evaluated with large lambda -captures, \texttt{std::vector} allocation and some simple co-routines. -The epxeriments are done on a linux PC using the gcc-11.2 compiler. +\noindent{}Each of the three will be evaluated with +\texttt{std::string}, \texttt{std::vector}, \texttt{std::function} and +co-routines.\\ +The experiments are done on a linux PC using the gcc-11.2 compiler. \section{Experiments} - ----------------------------------------- - - -No access to MMU, implicitly, prohibits calls to delete after -initialization phase. Otherwise this will lead to memory fragmentation -which again might lead to free-store depletion and ultimately -application failure. +This section describes the work done during the three experiments +along with any numerical or analytical results found. + +\subsection{Detecting Allocations}\label{detect} + +Searches on the Internet{\tm} has identified no ways of instructing +the compiler that ``no allocations allowed, fail if one is made''. +The reworked free-standing specification might make this possible in +the future, similarly to how exceptions and run-time type information +can be disabled with \texttt{-fno-exceptions} and \texttt{-fno-rtti}. +There exists a \texttt{-ffreestanding} argument, but this follows the +old (current) definition of free-standing and as such allows +dynamic allocation so it cannot be used. + +Some constructs have built-in optimizations that make them store their +data in local members, instead of on the free-store through a pointer, +until som limit is reached making it possible to use for +example \texttt{std::string}s or \texttt{std::function}s as long as +they don't require more than $N$ bytes, where $N$ is a compiler +dependent. + +To detect if a free-store allocation is done a simple overload +of \texttt{new} and \texttt{delete} can be done, which simply throws +an exeption if called. +This will lead to a run-time error and not a compile-time one as would +have been the ideal solution, but at least it can assists in finding +the $N$ for a specific compiler. + +This experiment can be found in the \texttt{noalloc.cc} file. The +following table shows the impirically deduced sizes of $N$ wherever +SSO/SBO is available along with comments about the general component +behaviour.\\ + +\noindent\begin{tabular}{| l | c | l |} +\hline +Component & $N$ & Comments \\ +\hline +\texttt{std::string} & $16$ & Exception propagates to caller. \\ +\texttt{std::vector} & N/A & No SBO, exception propagates to caller. \\ +\texttt{std::function} & $16$ & Exception doesn't seem to propagate to +caller (possible gcc bug). \\ +co-routine & N/A & No SBO, exception propagates to caller. \\ +\hline +\end{tabular} + +\subsection{Custom Allocator} + +\begin{verbatim} +Write allocator that uses stack-buffer and fails when depleted and try +it out (monotonic allocations) +List the components that supports it +\end{verbatim} Writing a custom allocator is only a solution to a sub-set of the allocations in an application, for example if all allocations are @@ -132,6 +179,17 @@ But for most applications (or at least most parts on an application) this is not the case, and therefore others means need to be taken into use. +std::string works, using basic_string, but is a bit clumsy + +std::vector works beautifully + +std::function as of c++17 can no longer be used with allocators + +Adding new operator to promise_type: +https://en.cppreference.com/w/cpp/language/coroutines#Dynamic_allocation + + +\subsection{Use \texttt{new} with Stack Buffer} The most common way of addressing this, is simply to only use stack allocation, or store all objects in as static globals. But in certain areas of the C++ language dynamic allocation might @@ -141,13 +199,26 @@ one example, but even more devious is the capture clause of a lambda, which might allocate extra memory, if more than $N$ members are captured, where $N$ is compiler dependent. -No way of telling the compiler that ``no allocations allowed, fail if -one is made'' exists, but one could wish for such a mechanism in the -wake of the ``free-standing C++'' subset work. -One thing is to prohibit use of language constructs that are -guaranteed to allocate, but quite another is to allow using constructs -in ways that doesn't make them allocate. -This, I think, is not part of the ``free-standing C++'' work. +Expanding on the overloaded \texttt{new} and \texttt{delete} from +section \ref{detect} a mechanism for using a stack-buffer for exactly +one call to \texttt{new} is made, looking something like this: +\begin{lstlisting}[language=C++] +void foo() +\end{lstlisting} + +Object owning the buffer and registering itself with the next call to +new. + +Scope of the stack object must be the same as the object using +it. Compiler does not help with that so this must be done by +convention. + +\section{Summing Up} + +Can be done, but is by no means elegant and can in some solution be +downright dangerous. +But if this is the price to pay to be able to use co-routines in +microcontrollers perhaps it is worth it none the less. \printbibliography diff --git a/a6/custom.cc b/a6/custom.cc new file mode 100644 index 0000000..4b95e0b --- /dev/null +++ b/a6/custom.cc @@ -0,0 +1,128 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "generator_stack.h" + +void* operator new([[maybe_unused]]std::size_t n) +{ + std::cout << "new\n"; + throw std::bad_alloc(); +} + +void operator delete(void*) throw() +{ + std::cout << "delete\n"; + // Do nothing. actual memory is allocated on the stack +} + +void operator delete(void*, std::size_t) throw() +{ + std::cout << "delete[]\n"; + // Do nothing. actual memory is allocated on the stack +} + +// Code heavily inspired by: +// https://stackoverflow.com/questions/66891368/template-parametric-type-allocator-in-c +template +struct StackAllocator +{ + using value_type = T; + using size_type = size_t; + using pointer = value_type*; + + template struct rebind { using other = StackAllocator; }; + + StackAllocator() = default; + StackAllocator(const StackAllocator&) = default; + template StackAllocator(const StackAllocator&) {} + + pointer allocate(size_type n) + { + if(n > S) throw std::bad_alloc(); + return buf; + } + + void deallocate(void*, size_type) + { + } + +private: + T buf[S]; +}; + +template +Generator iota(T start) +{ + while(true) + { + co_yield start++; + } +} + +int main() +{ + std::cout << " ** std::string:\n"; + { + std::basic_string, StackAllocator> str(31, 'a'); + std::cout << str << " " << sizeof(str) << '\n'; + + std::basic_string, StackAllocator> str2(31, 'a'); + std::cout << str2 << " " << sizeof(str2) << '\n'; + } + std::cout << '\n'; + + std::cout << " ** std::vector:\n"; + { + try + { + std::vector> vec{42}; + } + catch(std::bad_alloc &e) + { + std::cout << "No SBO for std::vector!\n"; + } + } + std::cout << '\n'; + + + std::cout << " ** std::function:\n"; + { +// { // Support for allocators with std::function removed in C++17 +// std::function, int()> f; +// char foo[16]{}; +// f = [foo]() // capture up to 16 bytes is ok for std::function +// { +// int i = 0; +// for(auto v : foo) i += v; +// return i; +// }; +// } + } + std::cout << '\n'; + + + std::cout << " ** co-routines:\n"; + { + try + { + auto gen = iota(0); + while(true) + { + auto i = gen(); + if(i > 10) break; + std::cout << i << " "; + } + std::cout << '\n'; + } + catch(std::bad_alloc &e) + { + std::cout << "Co-routines always allocate!\n"; + } + } + std::cout << '\n'; +} diff --git a/a6/generator_stack.h b/a6/generator_stack.h new file mode 100644 index 0000000..682b8e8 --- /dev/null +++ b/a6/generator_stack.h @@ -0,0 +1,94 @@ +// -*- c++ -*- +#pragma once + +// The code in this file has been taken directly from: +// https://en.cppreference.com/w/cpp/language/coroutines + +#include +#include +#include +#include + +template +struct Generator +{ + // The class name 'Generator' is our choice and it is not required for coroutine + // magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword. + // You can use name 'MyGenerator' (or any other name) instead as long as you include + // nested struct promise_type with 'MyGenerator get_return_object()' method. + + struct promise_type; + using handle_type = std::coroutine_handle; + + struct promise_type // required + { + T value_; + std::exception_ptr exception_; + + Generator get_return_object() + { + return Generator(handle_type::from_promise(*this)); + } + std::suspend_always initial_suspend() { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void unhandled_exception() { exception_ = std::current_exception(); } // saving + // exception + + template From> // C++20 concept + std::suspend_always yield_value(From&& from) + { + value_ = std::forward(from); // caching the result in promise + return {}; + } + void return_void() { } + + void* operator new(std::size_t n) + { + static char buf[S]; + std::cout << "alloc!\n"; + if(n < S) throw std::bad_alloc(); + return buf; + } + }; + + handle_type h_; + + Generator(handle_type h) + : h_(h) + { + } + ~Generator() { h_.destroy(); } + explicit operator bool() + { + fill(); // The only way to reliably find out whether or not we finished coroutine, + // whether or not there is going to be a next value generated (co_yield) + // in coroutine via C++ getter (operator () below) is to execute/resume + // coroutine until the next co_yield point (or let it fall off end). + // Then we store/cache result in promise to allow getter (operator() below + // to grab it without executing coroutine). + return !h_.done(); + } + T operator()() + { + fill(); + full_ = false; // we are going to move out previously cached + // result to make promise empty again + return std::move(h_.promise().value_); + } + +private: + bool full_ = false; + + void fill() + { + if (!full_) + { + h_(); + if (h_.promise().exception_) + std::rethrow_exception(h_.promise().exception_); + // propagate coroutine exception in called context + + full_ = true; + } + } +}; diff --git a/a6/noalloc.cc b/a6/noalloc.cc index 0da4025..898efb4 100644 --- a/a6/noalloc.cc +++ b/a6/noalloc.cc @@ -1,17 +1,12 @@ #include #include -// Universal allocator -namespace memory -{ -void* ptr{}; -} +#include "generator.h" -void* operator new([[maybe_unused]]std::size_t n) // throw(std::bad_alloc) - don't throw +void* operator new([[maybe_unused]]std::size_t n) { std::cout << "new\n"; - // Just return the supplied stack pointer - return memory::ptr; + throw std::bad_alloc(); } void operator delete(void*) throw() @@ -26,43 +21,95 @@ void operator delete(void*, std::size_t) throw() // Do nothing. actual memory is allocated on the stack } -//void foo() __attribute__((noinline)) +template +Generator iota(T start) +{ + while(true) + { + co_yield start++; + } +} + int main() { - char buf[256]; - memory::ptr = buf; + std::cout << " ** std::string:\n"; + { + std::string str(15, 'a'); // 15 characters + zero termination - ok + str = "123456789012345"; // 15 characters + zero termination - ok + try + { + str = "1234567890123456"; // 16 characters + zero termination + } + catch(std::bad_alloc &e) + { + std::cout << "Too big for SSO!\n"; + } + } + std::cout << '\n'; - std::cout << " ** strings:\n"; - { // strings - // now this is ok: - std::string str(32, 'a'); - std::cout << str << '\n'; + std::cout << " ** std::vector:\n"; + { + try + { + std::vector vec{42}; // even one element is too much, no SBO + } + catch(std::bad_alloc &e) + { + std::cout << "No SBO for std::vector!\n"; + } + } + std::cout << '\n'; - std::string str2(24, 'b'); - std::cout << str << '\n'; // the contents of str has been overwritten - // this will also allocate, but supply the same buffer - ok - str = "hello world hello world hello world"; + std::cout << " ** std::function:\n"; + { + { + std::function f; + char foo[16]{}; + f = [foo]() // capture up to 16 bytes is ok for std::function + { + int i = 0; + for(auto v : foo) i += v; + return i; + }; + } - // this is also ok, but due to SSO - std::string sso{"hello"}; +// try // this line seem to break gcc... +// { +// std::function f; +// char foo[17]{}; +// f = [foo]() +// { +// int i = 0; +// for(auto v : foo) i += v; +// return i; +// }; +// } +// catch(std::bad_alloc &e) +// { +// std::cout << "Too big for SBO in std::function!\n"; +// } } + std::cout << '\n'; + - std::cout << " ** lambdas:\n"; - { // lambdas - std::function f; + std::cout << " ** co-routines:\n"; + { + try { - char foo[16]{}; - f = [=]()__attribute__((noinline)) // capture up 16 bytes - ok - { - int i = 0; - for(auto v : foo) - { - i += v; - } - return i; - }; // capture foo by copy - inlined + auto gen = iota(0); + while(true) + { + auto i = gen(); + if(i > 10) break; + std::cout << i << " "; + } + std::cout << '\n'; + } + catch(std::bad_alloc &e) + { + std::cout << "Co-routines always allocate!\n"; } - [[maybe_unused]]auto x = f(); } + std::cout << '\n'; } -- cgit v1.2.3