侯捷先生在《漫谈程序员与编程》 中讲到 STL 运用的三个档次:“会用 STL,是一种档次。对 STL 原理有所了解,又是一个档次。追踪过 STL 源码,又是一个档次。第三种档次的人用起 STL 来,虎虎生风之势绝非第一档次的人能够望其项背。”
WIP
EASTL就是把STL重新实现一遍,其中:
过一遍EASTL源码仓库,以及《STL源码剖析》,提炼一些重点摘要
【游戏编程扯淡精粹】TinySTL源码阅读_游戏编程扯淡精粹-CSDN博客
无,反正我不会模板元编程,就是来STL现学的
构建脚本
set build_folder=out
mkdir %build_folder%
pushd %build_folder%
call cmake .. -DEASTL_BUILD_TESTS:BOOL=ON -DEASTL_BUILD_BENCHMARK:BOOL=OFF
call cmake --build . --config Release
call cmake --build . --config Debug
call cmake --build . --config RelWithDebInfo
call cmake --build . --config MinSizeRel
pushd test
call ctest -C Release
call ctest -C Debug
call ctest -C RelWithDebInfo
call ctest -C MinSizeRel
popd
popd
替换基本无感,运行性能也没有太大差别(CPU,内存)
游戏平台和游戏设计对游戏软件的要求不同于其他平台的要求。
其中最重要的是,游戏软件需要大量的内存,但实际可用的内存是有限的。
其次,游戏软件也面临着其他限制,例如较弱的处理器缓存,较弱的CPU和非默认内存对准要求。
结果是,游戏软件需要小心使用内存和CPU。 C++标准库的容器,迭代器和算法可能对各种游戏编程需求有用。然而,标准图书馆的缺点和疏忽导致它并不是高性能游戏软件的理想选择。
这些弱点中最重要的是分配器模型。 C++标准图书馆被扩展和部分重新设计为EASTL,以便以便携式和一致的方式解决这些弱点。本文介绍了游戏软件开发问题,目前C++标准库的弱点,以及EASTL的解决方案。
下面是一个清单,描述为什么STL不适合游戏开发:
总的来讲,STL注重的是标准,EASTL注重的是在游戏开发中的实践和性能
重点是allocator,以及容器的内存优化,内存对齐;然后是一些扩展
EASTL就是把STL重新实现一遍,大部分接口保持一致;allocator做了重度改造,没法一致;加了一些游戏需要的容器和功能;内部做了一些优化,重写来保证跨平台,可调试,性能优化
EASTL 的实施首先由以下按重要性顺序列出的指令指导。
请注意,与必须将正确性放在首位的商业 STL 实现不同,我们更重视效率。因此,某些功能可能具有其他类似系统中不存在的使用限制,但允许更有效的操作,尤其是在对我们重要的平台上。
可移植性很重要,但并不重要。是的,EASTL 必须在我们将为其发布游戏的所有平台上编译和运行。但我们并不认为这意味着所有可以想象用于此类平台的编译器。例如,Microsoft VC6 可以用来编译 Windows 程序,但是 VC6 的 C++ 对 EASTL 的支持太弱,所以在 VC6 下根本无法使用 EASTL。
EASTL 比许多其他模板库(尤其是 Microsoft STL 和 STLPort)实现了更好的可读性。我们尽一切努力使 EASTL 代码简洁明了。有时我们需要提供优化(特别是与 type_traits 和迭代器类型相关)会导致代码不那么简单,但效率恰好是我们的主要指令,因此它覆盖了所有其他考虑因素。
首先确定EA游戏发布的平台
然后在这些重点平台(包含编译器)上确保跨平台兼容性,以及性能最优化
其次,可读性是为了可维护性,可调试性,重写过程顺手完善的事情
标准化,和数学上的正确性,基本不考虑
简单地说 EASTL 是线程安全的或线程不安全的还不够简单。但是,我们可以说,在线程安全方面,EASTL 做了正确的事情。
单个 EASTL 容器不是线程安全的。也就是说,如果这些访问中的任何一个正在修改操作,那么同时从多个线程访问容器实例是不安全的。可以同时从多个线程以及任何其他独立数据结构中读取给定容器。如果用户希望能够从多个线程对容器实例进行修改访问,则由用户来确保发生正确的线程同步。这通常意味着使用互斥锁。
容器以外的 EASTL 类在线程安全方面与容器相同。EASTL 函数(例如算法)本质上是线程安全的,因为它们没有实例数据并且完全在堆栈上操作。在撰写本文时,没有 EASTL 函数分配内存,因此不会通过这种方式带来线程安全问题。
用户很可能需要关注内存分配方面的线程安全。如果用户从多个线程修改容器,那么分配器将被多个线程访问。如果分配器在多个容器实例(相同类型的容器或不同类型的容器)之间共享,那么用户用来保护对单个实例的访问的互斥锁(如上所述)将不足以为跨多个实例使用的分配器提供线程安全。这里的常规解决方案是在分配器中使用互斥锁,如果它被执行以供多个线程使用。
EASTL 既不使用静态变量也不使用全局变量,因此不存在会使用户难以实现线程安全的实例间依赖关系。
虽然 EASTL 通常优于标准 STL,但这里有一些基准测试表明 EASTL 比标准 STL 慢。对此有三种主要解释:
就是Python的operator模块
RenderItem按视距排序,不透明物体从近到远,透明物体从远到近
// 2 render queue, opaque and transparent, sorted by distance to camera
using OpaqueDrawables = std::multimap<float, RenderItem, std::less<float>>;
using TransparentDrawables = std::multimap<float, RenderItem, std::greater<float>>;
if (material.isBlendable()) {
transparentDrawables.emplace(distantToCamera, renderItem);
} else {
opaqueDrawables.emplace(distantToCamera, renderItem);
}
可以自动推导,省略T=int
std::greater<>()(6, 4) // -> true
第一个函数是核心实现,a>b
第二个函数是一个特化,做了输入和输出的类型自动推导(案例二)
template <typename T = void>
struct greater : public binary_function<T, T, bool>
{
EA_CPP14_CONSTEXPR bool operator()(const T& a, const T& b) const
{ return a > b; }
};
// http://en.cppreference.com/w/cpp/utility/functional/greater_void
template <>
struct greater<void>
{
template<typename A, typename B>
EA_CPP14_CONSTEXPR auto operator()(A&& a, B&& b) const
-> decltype(eastl::forward<A>(a) > eastl::forward<B>(b))
{ return eastl::forward<A>(a) > eastl::forward<B>(b); }
};
下面不讲如何自定义iterator了,我目前从来没遇到过需求(C#,Python,Lua),再加上C++ iterator比较难写,简单了解,按需深入
迭代器可以分为不同的种类,这是因为他们使用不同的算法,有5种迭代器。
例如,find()算法需要一个可以递增的迭代器,而reverse()算法需要一个可以递减的迭代器等。
常见容器,vector、deque提供的是随机访问迭代器,list提供的是双向迭代器,set和map提供的是向前迭代器。
有5种迭代器:
- 输入迭代器(Input Iterator):只能向前单步迭代元素,不允许修改由该迭代器所引用的元素;
- 输出迭代器(Output Iterator):只能向前单步迭代元素,对由该迭代器所引用的元素只有写权限;
- 向前迭代器(Forward Iterator):该迭代器可以在一个区间中进行读写操作,它拥有输入迭代器的所有特性和输出迭代器的部分特性,以及向前单步迭代元素的能力;
- 双向迭代器(Bidirectional Iterator):在向前迭代器的基础上增加了向后单步迭代元素的能力;
- 随机访问迭代器(Random Access Iterator):不仅综合以后4种迭代器的所有功能,还可以像指针那样进行算术计算;
封装常见algorithm对container的全迭代
#include
template<class C, class Func>
inline Func ForEach(C &c, Func f) {
return std::for_each(c.begin(), c.end(), f);
}
template<class C, class Func>
inline void EraseIf(C &c, Func f) {
c.erase(std::remove_if(c.begin(), c.end(), f), c.end());
}
template<class C, class T>
inline void Erase(C &c, const T &t) {
c.erase(std::remove(c.begin(), c.end(), t), c.end());
}
template<class C, class T>
inline auto Find(C &c, const T &value) {
return std::find(c.begin(), c.end(), value);
}
template<class C, class Pred>
inline auto FindIf(C &c, Pred p) {
return std::find_if(c.begin(), c.end(), p);
}
自己按需实现新的algorithm
template<typename MAP, typename K, typename V>
inline bool AddOrUpdate(MAP &m, const K &key, const V &val) {
typename MAP::iterator lb = m.lower_bound(key);
if (lb != m.end() && !m.key_comp()(key, lb->first)) {
// lb points to a pair with the given key, update pair's value
lb->second = val;
return false;
} else {
// no key exists, insert new pair
m.insert(lb, std::make_pair(key, val));
return true;
}
}
template <typename InputIterator, typename Function>
inline Function
for_each(InputIterator first, InputIterator last, Function function)
{
for(; first != last; ++first)
function(*first);
return function;
}
EASTL 所做的是使用一种更熟悉的内存分配模式,即只有一个分配器类接口,它被所有容器使用。此外,EASTL 容器允许您访问它们的分配器并查询它们、命名它们、更改它们等。
EASTL 选择在容器交换和分配操作期间使分配器不会在容器之间复制。这意味着如果容器 A 与容器 B 交换其内容,则两个容器都保留其原始分配器。类似地,将容器 A 分配给容器 B 会导致容器 B 保留其原始分配器。等效的容器应通过 operator==; 报告。如果分配器相等,EASTL 将进行智能交换,否则将进行暴力交换。
// EASTL allocator
class allocator
{
public:
allocator(const char* pName = NULL);
void* allocate(size_t n, int flags = 0);
void* allocate(size_t n, size_t alignment, size_t offset, int flags = 0);
void deallocate(void* p, size_t n);
const char* get_name() const;
void set_name(const char* pName);
};
allocator* GetDefaultAllocator();
就是malloc和free的类包装,对应std::allocator
,与std区别在于没用模板
啥也不干
class EASTL_API dummy_allocator
{
public:
EASTL_ALLOCATOR_EXPLICIT dummy_allocator(const char* = NULL) { }
dummy_allocator(const dummy_allocator&) { }
dummy_allocator(const dummy_allocator&, const char*) { }
dummy_allocator& operator=(const dummy_allocator&) { return *this; }
void* allocate(size_t, int = 0) { return NULL; }
void* allocate(size_t, size_t, size_t, int = 0) { return NULL; }
void deallocate(void*, size_t) { }
const char* get_name() const { return ""; }
void set_name(const char*) { }
};
inline bool operator==(const dummy_allocator&, const dummy_allocator&) { return true; }
inline bool operator!=(const dummy_allocator&, const dummy_allocator&) { return false; }
默认allocator,第一个是全局的默认分配器,第二个是每个类型特定的默认分配器
// GetStaticDefaultAllocator
EASTL_API allocator* GetDefaultAllocator();
EASTL_API allocator* SetDefaultAllocator(allocator* pAllocator);
// Example:
// MyAllocatorType* gpSystemAllocator;
// MyAllocatorType* get_default_allocator(const MyAllocatorType*) { return gpSystemAllocator; }
template <typename Allocator>
Allocator* get_default_allocator(const Allocator*);
EASTLAllocatorType* get_default_allocator(const EASTLAllocatorType*);
用malloc实现allocator
基本用法:vector
void* allocate(size_t n, int /*flags*/ = 0)
{ return malloc(n); }
void* allocate(size_t n, size_t alignment, size_t alignmentOffset, int /*flags*/ = 0)
{
if((alignment <= EASTL_SYSTEM_ALLOCATOR_MIN_ALIGNMENT) && ((alignmentOffset % alignment) == 0))
return malloc(n);
return NULL;
}
void deallocate(void* p, size_t /*n*/)
{ free(p); }
抽了一个基类,处理ctor中分配内存时抛出异常的情况
template <typename T, typename Allocator>
struct VectorBase
template <typename T, typename Allocator = EASTLAllocatorType>
class vector : public VectorBase<T, Allocator>
namespace TinySTL{
class vector{
private:
T *start_;
T *finish_;
T *endOfStorage_;
...
namespace eastl{
struct VectorBase{
protected:
T* mpBegin;
T* mpEnd;
eastl::compressed_pair<T*, allocator_type> mCapacityAllocator;
...
allocate
,placement new
uninitialized_copy
destruct
,deallocate
uninitialized_fill_n
struct ListNodeBase{
ListNodeBase* mpNext;
ListNodeBase* mpPrev;
}
template <typename T, typename Allocator>
class ListBase{
protected:
eastl::compressed_pair<base_node_type, allocator_type> mNodeAllocator;}
}
template <typename T, typename Allocator = EASTLAllocatorType>
class list : public ListBase<T, Allocator> {}
list就是一个基本的双向链表
链表这块实现方法比较多,之后对比一下侵入式
由于上述问题,deque源码就不看了
stack和queue是deque的子集,因此封装一下(或者不封装)deque就有了,也称为adapter
stack
所需操作
适配:vector, deque, string, list, intrusive_list
queue
所需操作
适配:deque, list, intrusive_list
template <typename T, typename Container = eastl::vector<T> >
class stack {}
template <typename T, typename Container = eastl::deque<T, EASTLAllocatorType, DEQUE_DEFAULT_SUBARRAY_SIZE(T)> >
class queue {}
由于只是简单封装,源码不看
heap的结构是完全二叉树,实际用vector表示
很没营养。。
EABase 是一小组定义独立于平台的数据类型和宏的头文件。因此,它类似于许多具有 platform.h、system.h、define.h 等文件的项目。不同之处在于 EABase 非常全面,并且是指定的 Electronic Arts 全球新项目标准。
关于基本类型和定义,其中许多已经出现在最新的 C 语言标准中,尽管 C++ 标准尚未正式采用它们。EABase 弥补了差距并定义了这些尚未定义的值。关于编译器和平台定义,EABase 提供了一种标准可靠的方法来识别或指定编译器、平台、字节序、对齐属性等。
您可能不想使用 float_t 和 double_t。它们的存在是为了与 C99 兼容,但您很少使用它们,因为它们的大小是可变的。
Prid8 等使用起来有些痛苦和丑陋,您可能会发现您不喜欢它们。它们也是为了 C99 兼容性。
intptr_t 不是指向 int 的指针;它是一个与指针大小相同的 int,因此您可以安全地在其中存储指针。
EA::result_type 很少使用并且存在是为了向后兼容。
在这里,我们列出了 EABase 定义的内容,按类别分组。这些定义在此文件顶部列出的文件修改日期之前是最新的。
BOOL8_T,INT8_T,UINT8_T,INT16_T,UINT16_T,INT32_T,UINT32_T,INT64_T,UINT64_T,FLOAT_T,DOUPY_T,(EASTDC PACKINGS IMPLING INT128_T)
INTPTR_T,UINTPTR_T,INTMAX_T,UINTMAX_T,SSIZE_T
CHAR8_T ,CHAR16_T,CHAR32_T
INT8_C(),UINT8_C(),UINT8_C(),等
INT8_MIN、INT8_MAX、UINT8_MAX 等
PRId8、PRId16、PRId32 等 SCNd8、SCNd16、SCNd32 等
EA::result_type
EA::SUCCESS, EA::FAILURE
EA_SUCCEEDED(), EA_FAILED()
EA_COMPILER_GNUC
EA_COMPILER_SN
EA_COMPILER_MSVC
EA_COMPILER_METROWERKS
EA_COMPILER_INTEL
EA_COMPILER_BORLANDC
EA_COMPILER_VERSION = <整数>
EA_COMPILER_NAME = <字符串>
EA_COMPILER_STRING = <字符串>
EA_COMPILER_NO_STATIC_CONSTANTS
EA_COMPILER_NO_TEMPLATE_SPECIALIZATION
EA_COMPILER_NO_TEMPLATE_PARTIAL_SPECIALIZATION
EA_COMPILER_NO_MEMBER_TEMPLATES
EA_COMPILER_NO_MEMBER_TEMPLATE_SPECIALIZATION
EA_COMPILER_NO_TEMPLATE_TEMPLATES
EA_COMPILER_NO_MEMBER_TEMPLATE_FRIENDS
EA_COMPILER_NO_VOID_RETURNS
EA_COMPILER_NO_COVARIANT_RETURN_TYPE
EA_COMPILER_NO_DEDUCED_TYPENAME
EA_COMPILER_NO_ARGUMENT_DEPENDENT_LOOKUP
EA_COMPILER_NO_EXCEPTION_STD_NAMESPACE
EA_COMPILER_NO_EXPLICIT_FUNCTION_TEMPLATE_ARGUMENTS
EA_COMPILER_NO_EXCEPTIONS
EA_COMPILER_NO_UNWIND
EA_COMPILER_IS_ANSIC
EA_COMPILER_IS_C99
EA_COMPILER_HAS_C99_TYPES
EA_COMPILER_IS_CPLUSPLUS
EA_COMPILER_MANAGED_CPP
EA_ALIGN_OF()
EA_PREFIX_ALIGN()
EA_POSTFIX_ALIGN()
EA_ALIGNED()
EA_PACKED()
EA_LIKELY()
EA_UNLIKELY()
EA_ASSUME()
EA_PURE
EA_WCHAR_T_NON_NATIVE
EA_WCHAR_SIZE
EA_RESTRICT
EA_DEPRECATED
EA_PREFIX_DEPRECATED
EA_POSTFIX_DEPRECATED
EA_FORCE_INLINE
EA_NO_INLINE
EA_PREFIX_NO_INLINE
EA_POSTFIX_NO_INLINE
EA_PASCAL
EA_PASCAL_FUNC()
EA_SSE = [0 | 1]
EA_IMPORT
EA_EXPORT
EA_OVERRIDE
EA_INIT_PRIORITY
EA_MAY_ALIAS
EA_PLATFORM_MAC
EA_PLATFORM_OSX
EA_PLATFORM_IPHONE
EA_PLATFORM_ANDROID
EA_PLATFORM_LINUX
EA_PLATFORM_WINDOWS
EA_PLATFORM_WIN32
EA_PLATFORM_WIN64
EA_PLATFORM_HPUX
EA_PLATFORM_SUN
EA_PLATFORM_NAME
EA_PLATFORM_DESCRIPTION
EA_PROCESSOR_POWERPC,EA_PROCESSOR_X86,EA_PROCESSOR_ARM等
EA_SYSTEM_LITTLE_ENDIAN,EA_SYSTEM_BIG_ENDIAN
EA_ASM_STYLE_ATT,EA_ASM_STYLE_INTEL,EA_ASM_STYLE_MOTOROLA
EA_PLATFORM_PTR_SIZE
EA_PLATFORM_WORD_SIZE