2020-08-21 第一次面试小结
独立开发近一年游戏没什么成果,最近开始找工作,今天面试了第一家公司,结果很糟糕,在这里记录反省。
公司位置在浦东世纪大道附近,约的 10 点面试,提早了一个小时到,就在楼下等到快 10 点再上去的。
面试过程没有笔试,面试官拿着一张面试题直接问我,很多题目没答好,有些是因为紧张,有些是真的不太了解。面试官最后直接说 “今天就这样吧,你 C# 基础太差”(不是原话,但是差不多)。以下为回顾的面试题
一、说说 lua 的一个元表
现在的理解是内置一个数组和一个字典的容器,可以用于实现继承。
二、手写一个栈
第一份工作有用过 lua,所以面试官上来就叫我用 lua 手写一个栈,我直接说大部分语法忘记了,面试官有点不耐烦地说那用 C# 写一个。这时候我还是没太反应过来是要我实现一个栈,我居然还以为他只是要我新建一个 Stack,还问他要什么类型的,还是不耐烦地回答我说 “int吧”。我觉得不对劲,再问了一遍才明白是要我实现一个栈功能的类,这个时候他应该以为我不知道栈,还强调了一遍后进先出。现在才刚刚想起来我当时的确弄混了栈和队列,写的是个队列。
出于紧张,一时间没有思路,问了一句能不能用其他数据结构,答不能,我懵了。面试官这个时候已经不想继续了,直接说 “不然今天就这样吧”。我再问一遍之后跟我说可以用数组,实际上这里是给我挖了一个坑。然后我就开始动手写,写的是个队列(狗头,现在回想真的太蠢了。不过面试官可能也没有看得很仔细,没有指出我实现的不是栈而是队列,但是指出了其他问题。
代码就不贴了,说说面试官讲的问题
1.用一个成员变量作为最后入队的下标,面试官说意义不明,而且没有初始化。意义不明是因为栈不需要队尾标记,初始化怪自己没认真检查。
2.出队的时候下标超出数组范围没有给出异常抛出。这个一方面是不应该用数组,另一方面我得好好看看这种异常情况除了打印以外,抛出要怎么写。
查阅后知道可以用链表实现链栈,用数组实现顺序栈,自己再撸一遍 C# 贴出来,先是链栈:
public class MyStack<T>
{public int Count{get { return count; }}private int count;private MyNode<T> _myNode;public void Clear(){_myNode = null;count = 0;}public bool Contains(T val){MyNode<T> checkNode = _myNode;while (checkNode != null){if (checkNode.val.Equals(val)) return true;checkNode = checkNode.lastNode;}return false;}public T Peek(){if (_myNode != null) return _myNode.val;throw new InvalidOperationException("Stack is empty.");}public T Pop(){if (_myNode != null){T val = _myNode.val;_myNode = _myNode.lastNode;count--;return val;}throw new InvalidOperationException("Stack is empty.");}public void Push(T val){MyNode<T> newNode = new MyNode<T>(val);newNode.lastNode = _myNode;_myNode = newNode;count++;}public T[] ToArray(){T[] arr = new T[count];MyNode<T> checkNode = _myNode;for (int i = 0; i < count; i++){arr[i] = checkNode.val;checkNode = checkNode.lastNode;}return arr;}private class MyNode<T>{public T val;public MyNode<T> lastNode;public MyNode(T val){this.val = val;}}
}
顺序栈:
class MyStack<T>
{private T[] _stackArr;private int _index;public int Count{get { return _index + 1; }}public MyStack2(){_stackArr = new T[8];_index = -1;}public void Clear(){_index = -1;}public bool Contains(T val){for (int i = 0; i < _index; i++){if (_stackArr[i].Equals(val)) return true;}return false;}public T Peek(){if (_index == -1) throw new InvalidOperationException("Stack is empty.");return _stackArr[_index];}public T Pop(){if (_index == -1) throw new InvalidOperationException("Stack is empty.");return _stackArr[_index--];}public void Push(T val){if (Count >= _stackArr.Length){T[] temp = new T[_stackArr.Length * 2];for (int i = 0; i < _stackArr.Length; i++){temp[i] = _stackArr[i];}_stackArr = temp;}_index++;_stackArr[_index] = val;}public T[] ToArray(){T[] ret = new T[Count];for (int i = 0; i < Count; i++){ret[i] = _stackArr[i];}return ret;}
}
晚些再用 lua 撸一遍。
三、DrawCall 是什么?如何减少 DrawCall?如何减少在 ui 中的 DrawCall 使用?顶点的坐标空间变换过程?
这个算是面试官随口问的,因为我简历上没写渲染相关。
第一个问题我随便回答的绘制过程。回头翻看了冯乐乐的《Unity Shader入门精要》,总结一下应该回答: CPU 对 GPU 发起的对一个图元列表的命令。
第二个问题上网搜了一圈,感觉 陈嘉栋(慕容小匹夫)大佬的文章 深入浅出聊优化:从Draw Calls到GC讲的比较详细。这里要注意一下,DrawCall 的优化实际是对 CPU 的优化。稍微总结一下方法以防以后再遇到类似问题:
1、静态批处理:使用相同材质,并且不需要移动的物体,都在 Inspector 右上角勾选 Static 的下拉菜单勾选 Batching Static 之后完成
静态批处理前:
静态批处理后:
2.动态批处理:材质、网格相同的两个物体 Unity 会自动进行批处理,在 Unity5 之前有对 Scale 限制,Unity5 之后没有,但是还有如下限制:
- 网格物体的顶点需要小于 900;
- 如果着色器使用了顶点位置、法线和 UV 值(纹理坐标)三种属性,只能批处理 300 顶点一下的物体,如果使用了更多的属性,最大顶点数量只能是 900 / n(n 为使用的属性数量);
- 多通道(多 pass)的 Shader 会妨碍批处理操作;
- 带有 lightmap(光照纹理)的物体需要指向 lightmap 的同一位置。
另外还需要在 Edit —> Project Settings —> Player —> Other Settings 中勾选 Dynamic Batching(Unity 2019.4.2f1)。
开启动态批处理前,左:相同的两个物体;右:不同的两个物体:
开启动态批处理后,左:相同的两个物体;右:不同的两个物体:
3.通过把纹理打包成图集来尽量减少材质的使用,具体操作看 weixin_44819220 大佬的文章 Unity3d 打包图集。
4.尽量少用反光、阴影一类效果,因为会使物体多次渲染。
第三个问题在 怣*痛 大佬的文章 UI优化 drawcall的优化 感觉整理较好,还是自己列举一下以防再次碰到:
1.减少 mask 的使用,每个 mask 会单独计算 DrawCall,可以考虑用带通道的图片代替 mask 的遮罩功能。
2.合并图集,DrawCall 会按照图集逐个进行批处理。
3.在一串层级关系中,带有来自相同图集图片的组件最好是父子层级关系,即两个组件之间没有其他层级插入,否则还是会进行多次批处理。
4.减少 UI 层级的深度。
5.常用的部分和不常用的部分分在两个节点下,动静分部。
7.尽可能去除组件的 Raycast Target(射线检测)属性。(效果不明显)
第四个问题我好像是答对了,具体还是参照 冯乐乐的《Unity Shader入门精要》,顺序是 模型空间—>世界空间—>观察空间—>裁剪空间(齐次裁剪空间)。
四、如何优化内存
在这个问题之前,面试官先问了对象池能用在什么地方,我就回答反复使用的物体,之后问怎么优化内存我就先答了刚刚说的对象池,然后含糊地说了减少模型顶点,string 改用 stringbuilder,答得很不完整。
看了 UWA 创始人张鑫巨佬的文章 性能优化,进无止境-内存篇(上)和 性能优化,进无止境---内存篇(下)。以下总结一下要点:
1.资源内存占用:
(1) 纹理:
A.纹理格式:根据硬件选择合适的纹理格式;
B.纹理尺寸:尽可能降低纹理尺寸;
C.UI资源的Mipmap功能:在UI纹理中开启Mipmp功能不会提升渲染效率,反而增加无用的内存占用,建议关闭;
D.Read & Write:开启纹理的 “Read & Write” 会使纹理内存增大一倍。
(2)网格:如果没有 Color、Normal、Tangent 等属性的 Mesh 和有这些属性的 Mesh 进行合并,没有这些属性的 Mesh 也将会被添加上这些属性,造成很大的内存开销。
2.引擎模块自身占用:较多开销在 WebStream 和 SerializeFile,绝大部分内存分配是由 AssetBundle 加载资源导致,要及时释放 AssetBundle 或者将解压后的 AssetBundle 数据存储于本地。
3.托管堆内存占用:Mono 不会把堆内存返还给系统。
4.内存泄漏:资源泄露,切换场景没有释放资源。
5.资源冗余:
(1)同样的资源被打入两个 AB 包;
(2)直接预制不同的 Material,而不是修改同一个 Material 的属性,因为修改属性会实例化一个新的 Material。
另外把网上到处都在转的面试题中优化内存部分也写在这里:
1.压缩自带类库;
2.将暂时不用的以后还需要使用的物体隐藏起来而不是直接 Destroy 掉(对象池);
3.降低模型的片面数、降低模型的骨骼数量、降低贴图的大小;
4.使用光照贴图,使用多层次细节(LOD),使用着色器(Shader),使用预设(Prefab)。
五、GC的底层原理
这个我完全没有答上来。
看了 carsonche 大佬的文章 Unity GC垃圾回收 和 zblade 大佬的文章 Unity优化之GC——合理优化Unity的GC,两位似乎都是翻译自官方文档,描述得比较简单一些。因为想强化记忆,所以在这里手打一遍大致内容。
简介:回收不再使用的内存空间。
原理:Unity 采用 Boehm GC(2019 之后加入 增量式垃圾回收),其使用 Mark-Sweep(标记-清除),即通过将使用中的内存标记后遍历清除未标记内存。具体标记过程依赖一个根对象遍历所有内存,标记结束后遍历整个堆内存清除未标记内存。GC 在堆(heap)中操作,堆栈栈(stack)中存储较小、生命周期较短的数据,堆与之相反,所以如果没有 GC 的话堆会一直增长。
堆栈的运行方式类似数据结构的 Stack,操作十分快捷简单,就不多做阐述。
堆的内存分配过程:
- 检测内存空间是否足够,如果有则直接分配,否则进入 2;
- 触发 GC 回收废弃的内存空间,再检测内存空间是否足够,如果有则分配,否则进入 3;
- 扩展堆内存的大小,分配内存空间。
GC 的具体操作(操作耗费大,堆上使用中的空间越多耗费越大):
- 通过根对象检查堆内存上所有存储变量;
- 检测变量的引用是否在激活状态;
- 把引用不在激活状态的变量,标记为可回收;
- 移除可回收的变量,回收对应的内存空间;
GC 触发时机:
- 上文的内存分配过程中的触发(所以频繁分配堆内存的话 GC 会被反复触发);
- 根据不同平台,以一定的频率自动触发;
- 手动强制执行回收。
优化:
- 尽量避免在 Update 或碰撞检测等函数中创建引用对象,可以用成员变量缓存反复使用;
- 数据结构的重置尽量使用清空,而不是重置;
- 使用对象池,避免反复创建和销毁需要反复使用的对象;
- string 每次操作都会分配内存,如果字符串有一部分经常改变,另一部分不会的话,把两个部分分开;
- 也可以用 StringBuilder 代替 String;
- 尽量介绍 Debug.Log() 等函数,它无论如何会产生一个字符串;
- 许多函数可以代替属性或者其他函数,可以避免这些属性或者函数返回时分配新的内存;
- 减少装箱操作,装箱操作会在堆内存分配一个 System.Object 类型的引用;
- 协程 yield return 返回参数时会产生不必要的内存垃圾,如果返回 null 则不会
- 协程如果要返回一个 new 对象的话,可以用缓存优化;
- 可以用其他方法代替协程,比如计时器可以写在 Update 中代替协程。
- Unity 5.5 之前的 foreach 需要在堆内存产生一个 System.Object ,尽量用 for 或者 while 代替;
- 减少函数引用(这个如果引用不是特别频繁的话,我觉得没有必要)。
六、协程和线程的区别
面试常见题了,凭着印象说:线程可以开起多条,协程任意指定时间只能有一个在运行(因为都在主线程当中),主线程以外的线程无法访问 Unity3D 的相关对象、方法、组件,线程安全需要加锁。面试官没说什么,我回答应该是不够的。
网上没有找到很满意的相关文章,之后又找到的话再放上来。
七、字典和列表的时间复杂度
面试官开始直接问我字典的时间复杂度是多少,我没反应过来,随口答了 O(nlogn),显然不对,只能说不知道,补了一句我只知道字典是用哈希表实现的。面试官又问了列表的时间复杂度是多少,我还是答不知道。之后面试官就叫我走了。
主要还是出于紧张,当时一直脑子一团乱没明白问的到底是啥,查阅后知道这个问题要答对数据结构进行添加、查找、Contains 等的时间复杂度,如果当时反应过来是回答这些的话,想一下就能想出来了。
字典 Dictionary:1.获取的时间复杂度为 O(1),因为使用哈希表索引。2.添加的时间复杂度再不需要扩容时为 O(1),需要扩容时为 O(n)。3.ContainsKey 的时间复杂度为 O(1),因为使用哈希表索引。4.ContainsValue 的时间复杂度为 O(n),因为需要遍历整个字典查找。
列表 List:1.获取的时间复杂度为 O(1),直接用整型下标索引获取得到。2.添加的时间复杂度同字典。3.Contains 的时间复杂度为 O(n),因为需要遍历列表查找。
2020-08-21 第一次面试小结相关推荐
- 《惢客创业日记》2020.08.21(周五)天使投资人的底限
今天上午,在凉粉儿的公司,与惢客的两位天使投资人"凉粉儿"和"汽水"做了一次很重要的沟通,虽然有了近两年的合作,但是,真正三个人坐在一起沟通惢客,还是头一回.这 ...
- 2020年4月2日华为消费者BG部门Android安卓第一次面试经验(一面,面经,编程代码题)
一.问题背景 博主于2020年4月7日参加了腾讯Android安卓第一次面试,以下为华为面试官当场让博主在15分钟内必须实现的编程代码题目. 二.2020年4月2日华为消费者BG部门Android安卓 ...
- 2020人工神经网络第一次作业
➤ 01第一题 1.问题描述 异或问题是一类简单的非线性可分问题,通过人工神经网络完成对异或逻辑关系的实现,可以帮助加深对人工神经网络算法的理解. ▲ 异或问题两种表示 ▲ 异或问题两种表示图示 相关 ...
- (十三:2020.08.28)CVPR 2015 追踪之论文纲要(译)
CVPR 2020 追踪之论文纲要(修正于2020.08.27) 讲在前面 论文目录 讲在前面 论坛很多博客都对论文做了总结和分类,但就医学领域而言,对这些论文的筛选信息显然需要更加精细的把控,所以自 ...
- (十一:2020.08.28)CVPR 2017 追踪之论文纲要(译)
CVPR 2017 追踪之论文纲要(修正于2020.08.28) 讲在前面 论文目录 讲在前面 论坛很多博客都对论文做了总结和分类,但就医学领域而言,对这些论文的筛选信息显然需要更加精细的把控,所以自 ...
- 关于第一次面试总结(嵌入式软件开发工程师)
第一次面试总结 首先,笔试: 一.问死锁是什么,死锁的原因有哪些?死锁的四个必要条件是神马?如何解开死锁? 死锁: 指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力 ...
- (十四:2020.08.28)CVPR 2014 追踪之论文纲要(译)
CVPR 2020 追踪之论文纲要(修正于2020.08.28) 讲在前面 论文目录 讲在前面 论坛很多博客都对论文做了总结和分类,但就医学领域而言,对这些论文的筛选信息显然需要更加精细的把控,所以自 ...
- 2020人工神经网络第一次作业-参考答案第九部分
本文是 2020人工神经网络第一次作业 的参考答案第九部分 ➤09 第九题参考答案 1.数据整理 根据char7data.txt中的文件将训练样本(21个字符)以及对应的输出值转化到两个矩阵:char ...
- 2020人工神经网络第一次作业-参考答案第八部分
本文是 2020人工神经网络第一次作业 的参考答案第八部分 ➤08 第八题参考答案 1.题目分析 (1) 数据下载 从https://www.cosy.sbg.ac.at/~pmeerw/Waterm ...
- 记录大三第一次面试经历
今天面试了一家不大的公司的java实习生,面试流程很简单,要求也不高,但是呢,由于之前没有进行过真正的面试,积累的经验比较少,该准备的没准备,导致结果很不好.经过一个好朋友的指点,找到了一些失误点,所 ...
最新文章
- mysql innodb表分区
- TiDB 源码阅读系列文章(十八)tikv-client(上)
- java扫描包下类_实现获取扫描指定包路径下的jar文件或class文件JarPojoHandler类示例代码...
- RichTextBox 右键显示 ContextMenuTrip
- 排序算法以及基本数据结构
- python数字类型floatcomplexint_浅谈python 四种数值类型(int,long,float,complex)
- c语言指针跨函数使用内存
- STM8S103FP6芯片PB_4/PB_5输出问题
- 【Unity3D日常BUG】Unity3D 中听不到声音解决方案
- c语言oj查重,GitHub - shawnsky/hshe: Online Judge System 在线评测系统 代码查重 作业质量...
- Intellij IDEA创建Scala项目
- [导入]MPQ 文件系统完成
- 错误-The server encountered an unexpected condition that prevented it from fulfilling the request
- 实现元素水平垂直居中的4种方法
- [NodeJS] Mongoose Populate 基本使用
- java实现海盗比酒量
- 等级测评——定级、等级划分及测评时间
- 「Python条件结构」使用if结构实现密码验证
- echarts dataView数据对齐及表格复制
- 统信uos安装粤政易
热门文章
- 网页数据抓取工具 (谷歌插件 web Scraper)
- 百度首页被tn劫持的办法有那些、两种解决百度劫持的方法
- Linux vim编辑器在哪,【Linux】Linux中VIM编辑器的使用
- 基于热传导方程的高温作业专用服装设计(三)
- 【我的OpenGL学习进阶之旅】Assimp库支持哪些3D模型格式?
- 《Spring Boot极简教程》第9章 Spring Boot集成Scala混合Java开发
- java编程过程——流程图
- ActiveReportsJS 2.2.1 中文特殊版
- java如何看手机型号,基于JAVA代码 获取手机基本信息(本机号码,SDK版本,系统版本,手机型号)...
- java实现人脸识别源码【含测试效果图】——前期准备工作及访问提示