作者:陆泽西-《Unity3D高级编程:主程手记》
第一章
1.1架构的意义
1、承载力;2、可扩展性;3、易用性;4、可伸缩性;5、容错
1.2设计
分层(spring web应用、Linux系统架构、TCP/IP分层架构)
分治(分治抽象)
1.3构建
网络->数据->资源管理->核心逻辑框架->框架。GF、ET。
第二章
2.1底层原理和机制
c#的运行机制:C#代码->编译器->IL(DLL文件)->CLR(加载DLL然后翻译)->10000101(机器语言)
IL2CPP:将IL翻译成C++代码,发布到不同平台时,调用对应平台的C++编译器,AOT,无法热更。
Mono:.Net的跨平台项目,性能底下,C#版本更新慢,无法使用新版本的特性。
三种编译模式:
JIT:Just-In-Time,程序运行时将CIL编译为机器码
AOT:Ahead-Of-Time,将IL编译成机器码并存储在文件中,提前编译。
完全静态编译:该模式只支持少数平台,基于AOT更进一步产生所有的机器码,这可以让程序在运行期间完全不需要JIT,适用于IOS,PS,Xbox等主机系统。
2.2List底层源码
构造部分:
[DebuggerTypeProxy(typeof(Mscorlib_CollectionDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T>
{
private const int _defaultCapacity = 4;
private T[] _items;
[ContractPublicPropertyName("Count")]
private int _size;
private int _version;
[NonSerialized]
private Object _syncRoot;
static readonly T[] _emptyArray = new T[0];
// Constructs a List. The list is initially empty and has a capacity
// of zero. Upon adding the first element to the list the capacity is
// increased to 16, and then increased in multiples of two as required.
public List() {
_items = _emptyArray;
}
// Constructs a List with a given initial capacity. The list is
// initially empty, but will have room for the given number of elements
// before any reallocations are required.
//
public List(int capacity) {
if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
Contract.EndContractBlock();
if (capacity == 0)
_items = _emptyArray;
else
_items = new T[capacity];
}
List内部是用数组实现,而非链表,并且没有指定指定capacity时,数组大小为0。
接下来看Add方法
//将给定对象添加到列表末尾,列表大小+1
//如果需要,在添加新元素前,列表容量增加一倍
public void Add(T item) {
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}
EnsureCapacity:
//如果列表当前容量小于min,则容量将增加到当前容量的两倍或min,以较大者为准
private void EnsureCapacity(int min) {
if (_items.Length < min) {
int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
//在遇到溢出前,允许列表增长到最大可能的容量(大约2GB)
//请注意,即使_items.Length由于Uint强制转换而溢出,此检查仍然有效
if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
if (newCapacity < min) newCapacity = min;
Capacity = newCapacity;
}
}
每次增加一个元素,先检查容量是否还够,不够就调用EnsureCapacity增加容量。
在EnsureCapacity()中,每次容量不够,都会扩容一倍,_defaultCapacity表示容量的默认值为4,扩充容量默认为4->8->16->32……..
List使用数组作为底层数据结构,优点是索引快,缺点是扩容时性能堪忧,每次针对数组的new操作都会造成内存垃圾,这会给GC带来负担
然后是Remove方法:
//删除给定索引处的元素,列表大小-1
public bool Remove(T item) {
int index = IndexOf(item);
if (index >= 0) {
RemoveAt(index);
return true;
}
return false;
}
//返回列表范围内给定值首次出现的索引
//该列表从头到尾向前搜索
//使用Object.Equles方法将列表中的元素与给定值进行比较
//此方法使用Array.IndexOf方法执行搜索
public int IndexOf(T item) {
Contract.Ensures(Contract.Result<int>() >= -1);
Contract.Ensures(Contract.Result<int>() < Count);
return Array.IndexOf(_items, item, 0, _size);
}
//删除给定索引处的元素,列表大小-1
public void RemoveAt(int index) {
if ((uint)index >= (uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException();
}
Contract.EndContractBlock();
_size--;
if (index < _size) {
Array.Copy(_items, index + 1, _items, index, _size - index);
}
_items[_size] = default(T);
_version++;
}
Remove的调用包含IndexOf和RemoveAt两个方法,前者是为了找到元素的索引位置,后者删除指定位置的元素。
原理就是使用Array.Copy对数组进行覆盖,IndexOf的实现是Array.IndexOf,它本身内部的实现就是按索引顺序从0到n对每个位置进行比较,复杂度为先行迭代O(n)。
其他的就不贴源码了。
然后是Insert,和Add一样,先检查容量,不够就扩容,Insert插入元素时,使用的是复制数组的形式,将数组里指定元素后面的所有元素向后移动一个位置。
Add、Remove、Insert、IndexOf等接口都是没有做优化的,使用的都是线性迭代的方式。如果频繁使用,效率就会降低,也会造成不少的内存冗余,使得GC承担更多压力
其他接口也是一样比如AddRange、RemoveRange等
其他:
索引器直接使用数组索引实现
Clear方法调用时并不删除数组,而是将数组中的所有元素置为默认值(0或者null)然后设置_size为0,用于虚拟地表示当前容量。清零会将对象的引用标记从此集合中移除,从这个角度看,调用Clear清零是必要的。
Contains方法使用线性查找遍历比较元素,如果一致就返回true,否则返回false。
ToArray重新创建一个指定大小的数组,将数组本身的内容复制(Array.Copy())到新数组上再返回,过多使用会造成大量的内存分配
Find和Contains一样是线性查找
Enumerator注意每次获取迭代器时,Enumerator都会被创建(foreach的坑,.Net4.0后已经修复,但仍然不建议大量使用)
Sort调用了Array.Sort(),排序方式为快速排序,效率为O(nlgn)。
-
List的效率其实并不高,大部分算法都是线性复杂度,遇到规模较大的计算时会导致性能问题
- 此外List的内存分配方式也不合理,当其中元素不断增加会多次重新分配数组,抛弃原有数组。调用GC时候会造成压力。最好在List初始化的时候就声明Capacity。
- List不是线程安全的,并发情况下无法检查_size++的执行顺序,在多线程情况下使用List时应该注意安全
- **真实情况是,List并不高效,但是足够通用
2.3Dictionary源码
使用哈希函数做KV映射。
冲突处理方法:开放定址法,再Hash法,拉链法,公共溢出区等,Dictionary使用的是拉链法。
成员字段:
[DebuggerTypeProxy(typeof(Mscorlib_DictionaryDebugView<,>))]
[DebuggerDisplay("Count = {Count}")]
[Serializable]
[System.Runtime.InteropServices.ComVisible(false)]
public class Dictionary<TKey,TValue>: IDictionary<TKey,TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback {
private struct Entry {
public int hashCode; // Lower 31 bits of hash code, -1 if unused
public int next; // Index of next entry, -1 if last
public TKey key; // Key of entry
public TValue value; // Value of entry
}
private int[] buckets;
private Entry[] entries;
private int count;
private int version;
private int freeList;
private int freeCount;
private IEqualityComparer<TKey> comparer;
private KeyCollection keys;
private ValueCollection values;
private Object _syncRoot;
底层数据结构为数组,实例化后内部内部数组是0个数组.
Add
public void Add(TKey key, TValue value) {
Insert(key, value, true);
}
private void Initialize(int capacity) {
int size = HashHelpers.GetPrime(capacity);
buckets = new int[size];
for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
entries = new Entry[size];
freeList = -1;
}
private void Insert(TKey key, TValue value, bool add) {
if( key == null ) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets == null) Initialize(0);
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length;
#if FEATURE_RANDOMIZED_STRING_HASHING
int collisionCount = 0;
#endif
for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (add) {
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
}
entries[i].value = value;
version++;
return;
}
#if FEATURE_RANDOMIZED_STRING_HASHING
collisionCount++;
#endif
}
int index;
if (freeCount > 0) {
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else {
if (count == entries.Length)
{
Resize();
targetBucket = hashCode % buckets.Length;
}
index = count;
count++;
}
entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = value;
buckets[targetBucket] = index;
version++;
#if FEATURE_RANDOMIZED_STRING_HASHING
#if FEATURE_CORECLR
// In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
// in this case will be EqualityComparer<string>.Default.
// Note, randomized string hashing is turned on by default on coreclr so EqualityComparer<string>.Default will
// be using randomized string hashing
if (collisionCount > HashHelpers.HashCollisionThreshold && comparer == NonRandomizedStringEqualityComparer.Default)
{
comparer = (IEqualityComparer<TKey>) EqualityComparer<string>.Default;
Resize(entries.Length, true);
}
#else
if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer))
{
comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
Resize(entries.Length, true);
}
#endif // FEATURE_CORECLR
#endif
}
主要看Insert,初始化调用HashHelper->GetPrime,他会返回一个size需要的最小质数值,首次定义为3每次扩容两倍-即3->7->17->37(每次都是质数)
对关键字key做hash操作,获得索引地址:
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;//防止hashCode为负数
int targetBucket = hashCode % buckets.Length;
调用函数获得Hash值后,含需要对Hash地址执行余数操作确保再数组长度方范围内不溢出。
然后找到指定key位置的链表进行遍历,找出空位置将Value插入
for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (add) {
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
}
entries[i].value = value;
version++;
return;
}
#if FEATURE_RANDOMIZED_STRING_HASHING
collisionCount++;
#endif
这就是拉链法的推入操作。如果数组空间不够(freeCount为0),则扩容
Remove
public bool Remove(TKey key) {
if(key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int bucket = hashCode % buckets.Length;
int last = -1;
for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (last < 0) {
buckets[bucket] = entries[i].next;
}
else {
entries[last].next = entries[i].next;
}
entries[i].hashCode = -1;
entries[i].next = freeList;
entries[i].key = default(TKey);
entries[i].value = default(TValue);
freeList = i;
freeCount++;
version++;
return true;
}
}
}
return false;
}
和插入类似,对key进行hash操作然后在链表里找匹配元素,Remove并没有对内存进行删减,而是将对应hash位置置为默认值,这是为了减少内存的频繁操作。
ContainsKey
public bool ContainsKey(TKey key) {
return FindEntry(key) >= 0;
}
private int FindEntry(TKey key) {
if( key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
}
}
return -1;
}
FindEntry使用和前面相同的方式查找key的hash位置,然后从链表里匹配元素,成功返回该索引地址。
TryGetValue与ContainsKey类似,以此获取对应的Value,然后对索引器进行重定义。
Hash函数的创建过程
//
// Note that logic in this method is replicated in vm\compile.cpp to ensure that NGen
// saves the right instantiations
//
[System.Security.SecuritySafeCritical] // auto-generated
private static EqualityComparer<T> CreateComparer() {
Contract.Ensures(Contract.Result<EqualityComparer<T>>() != null);
RuntimeType t = (RuntimeType)typeof(T);
// Specialize type byte for performance reasons
if (t == typeof(byte)) {
return (EqualityComparer<T>)(object)(new ByteEqualityComparer());
}
// If T implements IEquatable<T> return a GenericEqualityComparer<T>
if (typeof(IEquatable<T>).IsAssignableFrom(t)) {
return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<int>), t);
}
// If T is a Nullable<U> where U implements IEquatable<U> return a NullableEqualityComparer<U>
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) {
RuntimeType u = (RuntimeType)t.GetGenericArguments()[0];
if (typeof(IEquatable<>).MakeGenericType(u).IsAssignableFrom(u)) {
return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(NullableEqualityComparer<int>), u);
}
}
// See the METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST and METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST_LONG cases in getILIntrinsicImplementation
if (t.IsEnum) {
TypeCode underlyingTypeCode = Type.GetTypeCode(Enum.GetUnderlyingType(t));
// Depending on the enum type, we need to special case the comparers so that we avoid boxing
// Note: We have different comparers for Short and SByte because for those types we need to make sure we call GetHashCode on the actual underlying type as the
// implementation of GetHashCode is more complex than for the other types.
switch (underlyingTypeCode) {
case TypeCode.Int16: // short
return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(ShortEnumEqualityComparer<short>), t);
case TypeCode.SByte:
return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(SByteEnumEqualityComparer<sbyte>), t);
case TypeCode.Int32:
case TypeCode.UInt32:
case TypeCode.Byte:
case TypeCode.UInt16: //ushort
return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(EnumEqualityComparer<int>), t);
case TypeCode.Int64:
case TypeCode.UInt64:
return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(LongEnumEqualityComparer<long>), t);
}
}
// Otherwise return an ObjectEqualityComparer<T>
return new ObjectEqualityComparer<T>();
}
对于数字类型,他们实现了IEquatable接口,直接使用GenericEqualityComparer
和List一样,Dictionary并不是线程安全的
官方源码中有这样的解释:
**Hashtable has Multiple reader/single writer(MR/SW) thread safety built into
**certain methods and properties,-whereas Dictionary doesn't.If you're
**converting to consider whether callers may have taken a dependence on MR/SW
**thread safety.IF a writer lock is available,then that maybe used
**with a Dictionary to et the same thread safety guarantee.
Hashtable在多线程读/写中是线程安全的,而Dictionary不是。如果要在多个线程中共享Dictionary的读/写操作,就要自己写lock,以保证线程安全。
2.4浮点数的精度问题
- 数值比较不相等,浮点数在运算时无法准确定位到某个值,比如要么比0.23小,要么比他大,这样就不能使用==来做检查而是使用>、<,如果要用等于,则必须使用一个很小的浮动区间,abs(X-Y) <0.00001,即X -Y >float.Epsilon。
- 设备不同,不同平台和架构会导致浮点数的精度不一致
- 计算数值不确定,浮点数由于位数限制无法得到一个精确的数值,而是一个被截断的值,在某些情况期望的结果是1,但算出来可能是0.9999999999。
解决办法:
- 由一台机器(服务器)决定运算结果,认定这个值为准确值,并将结果传递给其他设备或模块。
- 改用int或long,把浮点数乘以10幂次得到更准确的整数,即把精度用整型表示,比如约定所有浮点数都乘以10000,1.5x10000=15000,用的时候再转回来。
- 定点数,即把整数位和小数位分开存
- 字符串代替浮点数,但缺点是CPU和内存消耗大,只能做少量计算
2.5委托和事件、拆箱和装箱
委托
delegate的实例,从功能上来讲,类似于C++的函数指针。Delegate继承自System.MulticastDelegate,其中有BeginInvoke,EndInvoke,Invoke三个函数,分别代表异步开始,异步结束,直接调用。
MulticastDelegate
该类不允许被继承,父类Delegate也是如此,Delegate类中有一个变量用于存储函数地址,当变量操作=符号时,将直接保存函数地址。他还重写了+=,-=两个操作符它对应了MulticastDelegate的Combine和Remove。当对这些操作符进行操作时,相当于把函数地址退入链表尾部或移出链表。
当委托被调用。委托实例会以此调用调用链中保存的函数并将参数带进去。
官方文档如下:
MulticastDelegate类中有一个已经连接好的delegate列表,被称为调用列表,它由一个或者更多个元素组成。当一个multicast delegate被启动调用时,所有在调用列表里的delegate都会按照它们出现的顺序被调用。如果在执行列表期间遇到一个错误,就会立即抛出异常并停止调用。
Event
在委托上又做一次封装,这样做是出于业务代码的维护考虑。
装箱和拆箱
简单来讲,就是将一个值类型(数字,结构体,bool等)转换为引用类型(class,string等)或反过来。
值类型的数据存储在stack上,直接持有。
引用类型的真实数据存储在heap,持有该地址的指针。
装箱的内部操作:在堆内存分配一个新的内存块,将值类型的值复制到堆内存中,返回指向这个堆内存地址的指针。
拆箱反过来就可以了。
避免方法:
- 使用struct
- 使用泛型
- 通过统一实现的接口提前拆装箱
2.6排序
快排:最坏情况O(n^2),不消耗额外内存
最大最小堆:二叉树、寻路算法
桶排序、基数排序
2.7搜索算法
二分法:要求列表有序,效率较好。
平衡二叉树、红黑树、B树
四叉树:主要运用在2D平面空间搜索和划分上
八叉树:3D空间
2.8业务逻辑优化技巧
-
List&Dictionary
List->Contains,insert,Remove都是线性查找。
设置初始大小避免扩容,对于Dictionary来说
注意GetHashCode的算力损耗。
-
Struct
Struct分配在stack上,stack是连续内存,并且返回外部调用后回收非常快。CPU读取数据对连续内存也是友好的
使用数组时Struct的值是内存连续的,有利于CPU缓存命中。但并非所有结构体都可以这样。如果结构体太大超过了缓存复制的数据块则缓存命中不再起作用。
-
ObjectPool、ReferencePool
-
字符串
每次动态创建一个string都会在堆内存中分配一块内存。
c#对string没有缓存机制,每次使用都要重新分配(连接、切割、组合),并且抛弃没有引用的string等待GC回收
自建缓存机制,使用键值对缓存常用或常量string
使用unsafe的方式处理string(fixed,指针)