C++语言的单元测试与代码覆盖率

对代码进行单元测试是几乎每个软件工程师都要完成的工作。本文以C++语言为基础,讲解如何进行单元测试并生成测试报告。

测试是软件开发过程中一个必须的环节,测试确保软件的质量符合预期。

对于工程师自己来说,单元测试也是提升自信心的一种方式。

直接交付没有经过测试的代码是不太好的,因为这很可能会浪费整个团队的时间,在一些原本早期就可以发现的问题上。而单元测试,就是发现问题一个很重要的环节。

本文以C++语言为基础,讲解如何进行单元测试并生成测试报告。

在工具上,我们会使用下面这些:

  • GCC
  • CMake
  • Google Test
  • gcov
  • lcov

为了方便本文的讲解,我专门编写了一个演示项目作为代码示例。

演示项目的源码可以在我的Github上获取:paulQuei/gtest-and-coverage。

你可以通过下面几条命令下载和运行这个项目:

要运行这个项目,你的机器上必须先安装好前面提到的工具。如果没有,请阅读下文以了解如何安装它们。

如果你使用的是Mac系统,下文假设你的系统上已经安装了brew包管理器。如果没有,请通过下面这条命令安装它:

演示项目的目录结构如下:

这里演示的内容是:以测试一个我们要提供的软件库为例,讲解如何对其进行单元测试并生成测试报告。

为了简单起见,这个软件库只有一个头文件和一个实现文件。

当然,在实际上的项目中,一个软件库会通常包含更多的文件,不过这并不影响我们要说明的问题。

演示项目中的文件说明如下:

文件名称说明make_all.sh入口文件,会执行:编译,测试和生成报告等所有工作CMakeLists.txt项目的编译文件googletest-release-1.8.1.zipgoogle test源码压缩包utility.h待测试的软件库的头文件utility.cpp待测试的软件库的实现文件unit_test.cpp对软件库进行单元测试的代码

演示项目在如下的环境中测试过。

  • MacBook Pro操作系统:macOS Mojave 10.14.1编译器:Apple LLVM version 10.0.0 (clang-1000.11.45.2)CMake:cmake version 3.12.1Google Test: 1.8.1lcov: lcov version 1.13
  • Ubuntu操作系统:Ubuntu 16.04.5 LTS编译器:gcc (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609CMake:cmake version 3.5.1Google Test:1.8.1lcov:lcov version 1.12

为了简化编译的过程,这里使用CMake作为编译工具。关于CMake的更多内容请参见请官网:https://cmake.org。

关于如何安装CMake请参见这里:Installing CMake。

另外,你也可以通过一条简单的命令来安装CMake:

Mac系统:

Ubuntu系统

由于篇幅所限,这里不打算对CMake做过多讲解,读者可以访问其官网或者在网络上搜寻其使用方法。

这里仅仅对演示项目中用到的内容做一下说明。演示项目中的CMakeLists.txt内容如下:

以编号为序,这段代码说明如下:

  1. 设置使用的CMake最低版本号为2.8.11。
  2. 指定项目的名称为”utility”,项目名称可以通过${CMAKE_PROJECT_NAME}进行引用。
  3. 指定使用C++11。
  4. 这里的三行是编译google test,并将其头文件路径和编译结果的库文件路径添加到环境中。因为后面在编译单元测试代码的时候需要用到。
  5. 添加–coverage到编译器flag中,这个参数是很重要的,因为这是生成代码覆盖率所必须的。关于该编译参数的说明见这里:Program Instrumentation Options。
  6. 编译我们的软件库,这里将生成libutility_lib.a库文件。
  7. 编译单元测试的可执行文件。
  8. 单元测试的可执行文件需要链接我们开发的软件库以及google test的库。另外,google test依赖了pthread,所以这个库也需要。

软件测试有很多种分类方式。从测试的级别来说,可以大致分为:

  • 单元测试
  • 集成测试
  • 系统测试

这其中,单元测试是最局部和具体的。它通常需要对代码中的每一个类和函数进行测试。

单元测试通常由开发者完成,需要针对代码逻辑进行测试。所以它是一种白盒测试。

xUnit是几种单元测试框架的总称。最早源于Smalltalk的单元测试框架SUnit,它是由Kent Beck开发的。

除此之外,还有针对Java语言的JUnit,针对R语言的RUnit。

在本文中,我们使用Google开发的xUnit框架:Google Test。

Google Test的项目主页在Github上:Github: Google Test。

实际上,这个项目中同时包含了GoogleTest和GoogleMock两个工具,本文中我们只会讲解第一个。

Google Test支持的操作系统包含下面这些:

  • Linux
  • Mac OS X
  • Windows
  • Cygwin
  • MinGW
  • Windows Mobile
  • Symbian

目前有很多的项目都使用了Google Test,例如下面这些:

  • Chromium projects
  • LLVM
  • Protocol Buffers
  • OpenCV
  • tiny-dnn

关于如何编译Google Test请参见这里:Generic Build Instructions。

为了便于读者使用,我们在演示项目中包含了Google Test 1.8.1的源码压缩包。并且在CMake文件中,同时包含了Google Test的编译和使用配置工作。

如果使用演示项目,读者将不需要手动处理Google Test的编译和安装工作。

为了便于下文说明,演示项目中包含了几个简单的函数。

可以从这里下载源码以便查看其中的内容:paulQuei/gtest-and-coverage。

演示项目中的软件库包含一个头文件和一个实现文件。头文件内容如下:

这个头文件说明如下:

  • 头文件包含了三个函数,前两个用来做int和double类型的四则运算。最后一个判断输入的年份是否是闰年。
  • 当然,在实际的工程中,前两个函数合并实现为一个泛型函数更为合适。但这里之所以分成两个,是为了查看代码覆盖率所用。
  • 关于闰年说明如下:能被4整除但不能被100整除的年份为普通闰年。能被100整除,也同时能被400整除的为世纪闰年。其他都不是闰年。例如:1997年不是闰年,2000年是闰年,2016年是闰年,2100不是闰年。

这三个函数的实现也不复杂:

接下来我们就要对上面这些代码进行测试了。

要使用Google Test进行测试,整个过程也非常的简单。只要进行下面三部:

  1. 创建一个测试用的cpp文件
  2. 为上面这个测试用的cpp文件编写Makefile(或者CMake文件)。同时链接:待测试的软件库gtest库gtest_main库pthread库(Google Test使用了这个库所以需要)
  3. 编写测试代码,编译并运行测试的可执行程序。

并且,测试代码写起来也非常的简单,像下面这样:

是的,就是这么简单的几行代码,就对整数四则运算的函数进行了测试。

TEST后面所包含的内容称之为一条case,通常我们会为每个函数创建一个独立的case来进行测试。一个测试文件中可以包含很多条case。同时,一条case中会包含很多的判断(例如EXPECT_EQ…)。

注意:在做单元测试的时候,保证每条case是独立的,case之间没有前后依赖关系是非常重要的。

当然,测试代码中包含的判断的多少将影响测试结果的覆盖率。所以在编写每条case的时候,我们需要仔细思考待测试函数的可能性,有针对性的进行测试代码的编写。

这段代码应该很好理解,它分别进行了下面这些测试:

  • 1 + 1 = 2
  • 2 – 1 = 1
  • 3 x 3 = 9
  • 10 / 2 = 5
  • 10 / 0 > 999999999

你可能会发现,这段代码里面甚至没有main函数。它也依然可以生成一个可执行文件。这就是我们链接gtest_main所起的作用。

在实际的测试过程中,你想判断的情况可能不止上面这么简单。下面我们来看看Google Test还能做哪些测试。

Google Test对于结果的判断,有两种形式:

  • ASSERT_*:这类判断是Fatal的。一旦这个判断出错,则直接从测试函数中返回,不会再继续后面的测试。
  • EXPECT_*:这类判断是Nonfatal的。它的效果是,如果某个判断出错,则输出一个错误信息,但是接下来仍然会继续执行后面的测试。

可以进行的判断方法主要有下面这些:

布尔判断

FatalNonfatal说明ASSERT_TRUE(condition)EXPECT_TRUE(condition)判断 condition 为 trueASSERT_FALSE(condition)EXPECT_FALSE(condition)判断 condition 为 false

二进制判断

FatalNonfatal说明ASSERT_EQ(expected, actual)EXPECT_EQ(expected, actual)判断两个数值相等ASSERT_NE(val1, val2)EXPECT_NE(val1, val2)val1 != val2ASSERT_LT(val1, val2)EXPECT_LT(val1, val2)val1 < val2ASSERT_LE(val1, val2)EXPECT_LE(val1, val2)val1 <= val2ASSERT_GT(val1, val2)EXPECT_GT(val1, val2)val1 > val2ASSERT_GE(val1, val2)EXPECT_GE(val1, val2)val1 >= val2

说明:

  • EQ:EQual
  • NE:Not Equal
  • LT:Less Than
  • LE:Less Equal
  • GT:Greater Than
  • GE:Greater Equal

字符串判断

FatalNonfatal说明ASSERT_STREQ(expected, actual)EXPECT_STREQ(expected, actual)两个C string相同ASSERT_STRNE(str1, str2)EXPECT_STRNE(str1, str2)两个C string不相同ASSERT_STRCASEEQ(exp, act)EXPECT_STRCASEEQ(exp, act)忽略大小写,两个C string相同ASSERT_STRCASENE(str1, str2)EXPECT_STRCASENE(str1, str2)忽略大小写,两个C string不相同

浮点数判断

FatalNonfatal说明ASSERT_FLOAT_EQ(exp, act)EXPECT_FLOAT_EQ(exp, act)两个float数值相等ASSERT_DOUBLE_EQ(exp, act)EXPECT_DOUBLE_EQ(exp, act)两个double数值相等ASSERT_NEAR(val1, val2, abs_err)EXPECT_NEAR(val1, val2, abs_err)val1和val2的差距不超过abs_err

异常判断

FatalNonfatal说明ASSERT_THROW(stmt, exc_type)EXPECT_THROW(stmt, exc_type)stmt抛出了exc_type类型的异常ASSERT_ANY_THROW(stmt)EXPECT_ANY_THROW(stmt)stmt抛出了任意类型的异常ASSERT_NO_THROW(stmt)EXPECT_NO_THROW(stmt)stmt没有抛出异常

在某些情况下,我们可能希望多条测试case使用相同的测试数据。例如,我们的演示项目中,每条case都会需要创建Utility对象。

有些时候,我们要测试的对象可能很大,或者创建的过程非常的慢。这时,如果每条case反复创建这个对象就显得浪费资源和时间了。此时,我们可以使用Test Fixture来共享测试的对象。

要使用Test Fixture我们需要创建一个类继承自Google Test中的::testing::Test。

还记得我们前面说过,我们要尽可能的保证每条测试case是互相独立的。但是,当我们在多条case之间共享有状态的对象时,就可能出现问题。

例如,我们要测试的是一个队列数据结构。有的case会向队列中添加数据,有的case会从队列中删除数据。case执行的顺序不同,则会导致Queue中的数据不一样,这就可能会影响case的结果。

为了保证每条case是独立的,我们可以在每条case的执行前后分别完成准备工作和清理工作,例如,准备工作是向队列中添加三个数据,而清理工作是将队列置空。

这两项重复性的工作可以由::testing::Test类中的Setup和TearDown两个函数来完成。

我们演示用的Utility类是无状态的,所以不存在这个问题。因此,这里我们仅仅在Setup和TearDown两个函数中打印了一句日志。

使用Test Fixture后,我们的代码如下所示:

这段代码说明如下:

  1. Setup和TearDown两个函数标记了override以确认是重写父类中的方法,这是C++11新增的语法。
  2. 我们的Utility类是无状态的,因此Setup和TearDown两个函数中我们仅仅打印日志以便确认。
  3. 将Utility util设置为protected以便测试代码中可以访问。(从实现上来说,测试case的代码是从这个类继承的子类,当然,这个关系是由Google Test工具完成的)。

要使用这里定义的Test Fixture,测试case的代码需要将开头的TEST变更为TEST_F。

这里_F就是Fixture的意思。

使用TEST_F的case的代码结构如下:

这里的TestCaseName必须是Test Fixture的类名。

所以我们的测试代码写起来是这样:

我们针对ArithmeticCalculation方法故意只进行了一种情况的测试。这是为了最终生成代码覆盖率所用。

编写完单元测试之后,再执行编译工作便可以运行测试程序以查看测试结果了。

测试的结果像下面这样:

如果测试中包含了失败的case,则会以红色的形式输出。同时,会看到失败的case所处的源码行数,这样可以很方便的知道哪一个测试失败了,像下面这样:

只想有选择性的跑部分case,可以通过–gtest_filter参数进行过滤,这个参数支持*通配符。

像下面这样:

如果想要更好的理解这些内容。请读者下载演示项目之后完成下面这些操作:

在utility.h和utility.cpp中添加一些新的函数。在新添加的函数中故意包含一个bug。为新添加的函数编写测试代码,并测试出函数中包含的bug。

在进行单元测试之后,我们当然希望能够直观的看到我们的测试都覆盖了哪些代码。

理论上,如果我们能做到100%的覆盖我们的所有代码,则可以说我们的代码是没有Bug的。

但实际上,100%的覆盖率要比想象得困难。对于大型项目来说,能够达到80% ~ 90%的语句覆盖率就已经很不错了。

先来看一下,当我们在说“覆盖率”的时候我们到底是指的什么。

实际上,代码覆盖率有下面几种类型:

  • 函数覆盖率:描述有多少比例的函数经过了测试。
  • 语句覆盖率:描述有多少比例的语句经过了测试。
  • 分支覆盖率:描述有多少比例的分支(例如:if-else,case语句)经过了测试。
  • 条件覆盖率:描述有多少比例的可能性经过了测试。

这其中,函数覆盖率最为简单,就不做说明了。

语句覆盖率是我们最常用的。因为它很直观的对应到我们写的每一行代码。

而分支覆盖率和条件覆盖率可能不太好理解,需要做一下说明。

以下面这个C语言函数为例:

这个函数中包含了一个if语句,因此if语句成立或者不成立构成了两个分支。所以如果只测试了if成立或者不成立的其中之一,其分支覆盖率只有 1/2 = 50%。

而条件覆盖率需要考虑每种可能性的情况。

对于if (a && b)这样的语句,其一共有四种可能的情况:

  1. a = true, b = true
  2. a = true, b = false
  3. a = false, b = true
  4. a = false, b = false

请读者思考一下:对于三层if嵌套,每个if语句包含三个布尔变量的代码,如果要做到100%的条件覆盖率,一共要测试多少种情况。

很显示,在编写代码的时候,尽可能的减少代码嵌套,并且简化逻辑运算是一项很好的习惯。

便于测试的代码也是便于理解和维护的,反之则反。

有了这些概念之后,我们就可以看懂测试报告中的覆盖率了。

gcov是由GCC工具链提供的代码覆盖率生成工具。它可以很方便的和GCC编译器配合使用。

通常情况下,安装好GCC工具链,也就同时包含了gcov命令行工具。

对于代码覆盖率工具所做的工作,可以简单的理解为:标记一次运行过程中,哪些代码被执行过,哪些没有执行。

因此,即便没有测试代码,直接运行编译产物也可以得到代码的覆盖率。只不过,通常情况下这样得到的覆盖率较低罢了。

使用

这里我们以另外一个简单的代码示例来说明gcov的使用。

这段代码如下:

这是一个仅仅包含了main函数的c语言代码,main函数的逻辑也很简单。

我们将这段代码保存到文件test.c。

要通过gcov生成代码覆盖率。需要在编译时,增加参数–coverage:

–coverage等同于编译参数-fprofile-arcs -ftest-coverage以及在链接时增加-lgcov。

此处的编译结果除了得到可执行文件a.out,还会得到一个test.gcno文件。该文件包含了代码与行号的信息,在生成覆盖率时会需要这个文件。

很显然,带–coverage编译参数得到的编译产物会比不带这个参数要包含更多的信息,因此编译产物会更大。所以这个参数只适合在需要生成代码覆盖率的时候才加上。对于正式发布的编译产物,不应该添加这个编译参数。

当我们执行上面编译出来的可执行文件a.out时,我们还会得到每个源码文件对应的gcda后缀的文件。由test.gcno和test.gcda这两个文件,便可以得到代码的覆盖率结果了。

关于这两个文件的说明请参见这里:Brief description of gcov data files

只需要通过gcov指定源文件的名称(不需要带后缀):gcov test,便可以得到包含覆盖率的结果文件 test.c.gcov了。

回顾一下我们刚刚的操作内容:

我们可以cat test.c.gcov一下,查看覆盖率的结果:

这个结果应该还是很容易理解的,最左边一列描述了代码的覆盖情况:

  • -: 表示该行代码被覆盖了
  • 整数: 表示被执行的次数
  • #####:表示该行没有被覆盖

gcov得到的结果是本文形式的。但很多时候,我们可能希望得到更加美观和便于浏览的结果。

此时就可以使用lcov了。

lcov是gcov工具的图形前端。它收集多个源文件的gcov数据,并生成描述覆盖率的HTML页面。生成的结果中会包含概述页面,以方便浏览。

lcov支持我们前面提到的所有四种覆盖率。

这个链接是lcov生成的报告样例:lcov – code coverage report。

安装

lcov并非包含在GCC中,因此需要单独安装。

Mac系统

Ubuntu系统

使用

对于lcov的使用方法可以通过下面这条命令查询:

通过输出我们可以看到,这个命令的参数有简短(例如-c)和完整(例如–capture)两种形式,其作用是一样的。

这里主要关注的下面这几个参数:

  • -c 或者 –capture 指定从编译产物中收集覆盖率信息。
  • -d DIR 或者 –directory DIR 指定编译产物的路径。
  • -e FILE PATTERN 或者 –extract FILE PATTERN 从指定的文件中根据PATTERN过滤结果。
  • -o FILENAME 或者 –output-file FILENAME 指定覆盖率输出的文件名称。

另外还有需要说明的是:

  • lcov默认不会打开分支覆盖率,因此我们还需要增加这个参数来打开分支覆盖率的计算:–rc lcov_branch_coverage=1
  • lcov输出的仍然是一个中间产物,我们还需要通过lcov软件包提供的另外一个命令genhtml来生成最终需要的html格式的覆盖率报告文件。同样的,为了打开分支覆盖率的计算,我们也要为这个命令增加–rc lcov_branch_coverage=1参数

最后,make_all.sh脚本中包含的相关内容如下:

这段代码从我们前面编译的结果中收集覆盖率结果,并将结果输出到coverage.info_tmp文件中。但是这里面会包含非项目源码的覆盖率(例如google test),所以我们又通过另外一条命令来指定”src”文件夹进行过滤。最后,通过genhtml得到html格式的报告。

可以通过浏览器查看覆盖率报告的结果,像下面这样:

从这个报告的首页,我们已经可以看到代码的语句覆盖率(Lines),函数覆盖率(Functions)以及分支覆盖率(Branches)。而对于条件覆盖率可以从详细页面中看到。如下图所示:

在上面这张图中,我们可以看到哪些代码被覆盖了,哪些没有。而对于对于if-else之类的语句,也能很清楚的看到条件覆盖率的覆盖情况。例如,对于代码的27行,只覆盖了if成立时的情况,没有覆盖if不成立时的情况。

本文中,我们已经完整的完成了从编写单元测试到覆盖率生成的整个过程。

但实际上,对于这项工作我们还可以做得更多一些。例如下面这两项工作:

Google Mock是Google Test的扩展,用于编写和使用C++ Mock类。

在面向对象的编程中,Mock对象是模拟对象,它们以预先设定的方式模仿真实对象的行为。程序员通常会创建一个Mock对象来测试某个其他对象的行为,这与汽车设计师使用碰撞测试假人来模拟人类在车辆碰撞中的动态行为的方式非常相似。

关于Google Mock的更多内容请参见:Google Mock的文档。

对于演示项目的覆盖率报告是通过手动执行脚本文件生成的。

而在实际的项目中,可能同时有很多人在开发同一个项目,每一天项目中都会有很多次的代码提交。我们不可能每次手动的执行编译和生成覆盖率报告结果。这时就可以借助一些持续集成的工具,定时自动地完成项目的编译,测试和覆盖率报告结果的生成工作。

可以在持续集成工具中包含我们编写的脚本,然后将覆盖率报告的html结果发布到某个Web服务器上,最后再以邮件的形式将链接地址发送给大家。

这样就可以很方便的让整个团队看到所有模块的测试结果和覆盖率情况了。

完成了一整套这样的工作,可以非常好的提升整个项目的质量。

单片机c语言基础知识,c语言必背100代码有哪些?

我记得刚开始接触编程的时候,觉得太难了。

也很好奇,写代码的那些人也太厉害了吧?全是英文的,他们的英文水平一定很好吧?

他们是怎么记住这么多代码格式的?而且错了一个标点符号,整个程序都会有影响。

一个程序几千行,错一个标点符号都不行这也太难了吧?

带着新手的灵魂拷问,作为从业单片机编程10年的开发者,我来为大家拨开云雾。

看完以后你就会明白,其实他们也没那么厉害!即便你是初中文凭,也照样能编程。

对于c语言来说,要记得东西其实不多,基本就是几个常用语句加一些关键字而已。

你所看到的那些几千甚至上万行的代码,都是用这些语句和关键词来重复编写的。

只是他们逻辑功能不一样,另外的那些英文,都是程序员自己起的,比如说一些变量的名字,函数的名字。

如果你喜欢你定义成abc都可以,只不过为了程序大了以后为了方便阅读和维护,我们一般起跟要实现的功能相同的英文缩写代替。

比如说我要存储一个电压值,那你可以定义一个变量,名字叫VoltageValue,或者这两个词的缩写VoltVal。

所以,大家不要把这个想得这么神秘和高大上。

而从事单片机编程,需要的编程水平比做纯软件还要低一些,毕竟单片机的内存很有限,太大的程序也放不下。

单片机的c语言基础知识大家可以参考以下目录学习:

这个教程找无际单片机编程就可以拿到。

这是课程就是针对零基础的小伙伴学习的,先从认识单片机开始,然后再到计算机的存储和计算方式二进制、十进制。

学完这些基础,开始学习c语言的基本语法。

在学习过程中,大家一定要勤动手,多在单片机开发板上面做实验。

千万不要让知识点停留看懂的层面,看懂和自己能写出来区别还是很大的,请相信我!

如果你这些都学完了,可以在开发板上做个小实验,比如说电子时钟之类的。

这个步骤非常有必要,写的过程可以培养你的项目思维,以及代码整合能力。

如果这步直接跳过,后续学习更高级的stm32就会碰到很多问题,最大的问题就是你都学完了做项目还是无从下手。

单个功能都能实现,但整合起来就不知道怎么做了。

这就是缺少项目思维和代码整合能力导致的,不要一味地去追求学习高端的单片机,等你基础打扎实了,你会发现高端的也不过如此,就是外设多一点,资源丰富一点。

甚至说你会觉得高端的用起来比低端的单片机更加方便。

典型的就是stm32和51的对比,stm32单片机都是基于固件库开发,也就是原厂提供一套程序给你,你按照他们的函数调用就能把stm32单片机的资源用起来。

而51单片机就需要你去配置他们最底层的寄存器才能把资源用起来。

相对来说,stm32用熟了,开发效率会比51的高很多倍。

最后来聊下,有些新手说的c语言必背100个代码。

这纯粹是在瞎扯淡,c语言只是工具,不同的人用这个工具去实现同一种功能,他们的实现方法可能是不一样的,因为每个人的思维不同。

这不像我们课文靠背的,也没必要去背,看代码最重要的是理解这段代码实现功能的思路和原理。

通过功能和原理去理解代码,而不是通过代码去理解功能原理。

这个顺序搞不对,你一辈子都学不好编程。

会写代码的AI开源!C语言比Codex写得好,掌握12种编程语言丨CMU

萧箫 发自 凹非寺量子位 | 公众号 QbitAI

比Codex还会写C语言的AI代码生成模型,现在开源了!

这段时间,用AI写代码可以说是大火,其中最著名的要属OpenAI的Codex和DeepMind的AlphaCode。

△基于Codex的Copilot

然而,这两个AI模型,全都没有开源:

其中AlphaCode只给出了一些测试样例,而Codex只开放了API。

为此,来自CMU的几个研究人员,用GPT-2搞出了一个名叫PolyCoder的AI代码生成模型,而且还是开源的

据研究人员表示,虽然PolyCoder最大只有27亿参数(相比Codex有120亿参数),但它用C语言写出来的代码,比Codex的效果还要好。

这里面究竟有什么秘诀?

首先来看训练用的数据集,这也是PolyCoder的最大特点之一。

此前,包括Codex、CodeParrot等AI代码生成模型,主要都是基于Python语言的代码来训练。

例如Codex的评估数据集之一HumanEval,评估的也是生成Python代码的效果。

相比之下,PolyCoder采用了多种编程语言代码集来训练,一共有12种:

C、C#、C++、Go、Java、JavaScript、PHP、Python、Ruby、Rust、Scala和TypeScript。

其中,C语言的代码量是最多的,达到了221GB;而Python代码的数据量比Codex和CodeParrot用得都要少。

这里PolyCoder用的是GitHub上的公开代码,主要选取的是各种编程语言中比较受欢迎的库,每个库至少有50 Stars。

据研究人员表示,每种编程语言库的Stars总数加起来不超过25k,以避免模型生成的代码效果太过于倾斜最流行的编程语言(通常编程语言越流行,库的Stars就越多)

通过提取库中的文件、经过简单处理(包括消除重复代码)后,一共筛选出大约254GB的数据用于训练。

然后是预训练的方法。

语言模型的预训练方法通常有三种。

第一种是自左向右的语言模型,根据上文预测下文,比较适用于代码生成等;第二种是掩蔽语言模型,基于上下文预测屏蔽片段,比较适合代码分类等;第三种是编解码器模型,比较适用于代码注释等任务。

这里PolyCoder主要采用的是第一种预训练方法。

相比于同样采用GPT-2训练的CodeParrot和Codex,PolyCoder在超参数设置上也稍微有一些差异:

PolyCoder一共提供了三种不同的模型,分别有27亿参数、4亿参数和1.6亿参数,研究人员可以根据自身需求和不同的训练能力来选取合适的模型。

那么,最终训练出来的AI模型,代码生成效果如何?

研究人员将PolyCoder与已有的AI代码生成模型进行了对比。

由于AlphaCode不好比较(接口没开放),所以研究人员主要分析了下面这些模型,包括GPT-Neo、CodeParrot和Codex等。

其中蓝色的是开源的,橙色的是没开源的:

从参数量来看,PolyCoder并不是最顶尖的,最大的27亿参数模型也只有Codex的四分之一不到。

研究人员先是用语言模型评估常用的困惑度对一系列模型进行了比较。

困惑度(Perplexity),用于衡量语言模型(LM)的好坏。困惑度越低,语言模型面对代码感到困惑的程度就越低,模型生成效果越好。

从图中来看,PolyCoder在C语言中意外取得了最好的效果(困惑度最低)

用大量C语言训练PolyCoder的结果说明,即使模型整体原理不变(基于GPT-2),单纯改变训练用的代码集,也能训练出擅长不同语言风格的AI代码生成模型。

可惜的是,从其他语言来看,生成的效果就完全没办法和Codex相比了:

例如,在主要用于评估Python代码的HumanEval上,PolyCoder的能力远不如Codex好:

据论文分析,这可能是Python代码数据量、模型参数量不足等原因导致的。

此外,作者们也提到,做出PolyCoder的目的主要还是为了开源一个AI代码生成模型,让更多人参与研究和使用。

目前代码已经开源,无论是直接拿来用,还是试着在它的基础上开发新模型都可以。

感兴趣的小伙伴可以上手一试了~

一作许方正(Frank Xu),目前在CMU读博,研究方向是NLP、信息抽取等,发表过多篇顶会论文,包括ICLR、ACL和EMNLP等。本硕毕业于上海交通大学,师从朱其立教授。

Uri Alon,在CMU进行博士后工作,研究方向是编程语言处理(PLP)、NLP和深度学习。

Graham Neubig,CMU助理教授,研究方向是NLP、机器翻译和基于机器学习的自然语言理解。

Vincent J. Hellendoorn,CMU计算机助理教授,主要研究方向是软件工程和机器学习,致力于利用智能方法帮助软件开发人员减少代码调试、程序优化等繁琐工作的时间。

不知道作者们是否已经在用这个AI撸代码了(手动狗头)

项目地址:https://github.com/VHellendoorn/Code-LMs

论文地址:https://arxiv.org/abs/2202.13169

— 完 —

量子位 QbitAI · 头条号签约

关注我们,第一时间获知前沿科技动态

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。