Practical application of user experience optimization on special effects side - Package size

Practical application of user experience optimization on special effects side - Package size

​1 The size of special effects packages for TikTok

1.1 What is package size in one sentence?

Package size mainly refers to the size of the application installation package, such as the installation size displayed by the installation package in the App Store.

1.2 Why should we optimize the package size?

As the capabilities of applications are updated and iterated, the size of application installation packages will gradually increase, the traffic consumption and charges for users downloading applications will further increase, and the user's willingness to download will decrease relatively. On the other hand, as the package size increases, the time to install the application will become relatively longer, affecting the user experience. For low-end mobile phones with smaller ROMs, the application will occupy more memory after decompression, and some mobile phone managers will prompt insufficient memory and prompt uninstallation, which directly affects user usage.

1.3 Contribution of special effects to package size in TikTok

Douyin currently consists of multiple business lines, each of which plays a role similar to the middle platform. The special effects middle platform is one of the links of Douyin. Currently, special effects are aggregated from effect and lab into EffectSDK, which accounts for a proportion of the settlement package volume in Douyin as an independent business line.

1.4 Package volume composition on the special effects side

The package size of EffectSDK consists of two parts: binary files (i.e. executable files) and other resource files (images, configuration files, etc.). Binary files are mainly executable files generated by code, and resource files refer to built-in model files, material files, configuration files, etc.

As a middle platform, the binary code in the special effects EffectSDK occupies the vast majority of the volume. Different from the package size optimization ideas of applications such as Douyin and Toutiao, special effects can do relatively little in terms of resource compression. Since special effects are used as a middle platform to support Douyin's business, and provide special effects capabilities in the form of a library, there is a lot of room for deleting useless resources, removing useless codes, and optimizing code. Therefore, the performance optimization on the special effects side mainly focuses on minimizing the package size while supporting multiple functions, improving code quality, and achieving a balance between code efficiency and code volume.

2 Background knowledge of package size optimization

The special effects side in Douyin is supported by C++ code writing, which generates a static library after compilation and finally links to the executable file. In the process from code to binary file, the compiler does the preprocessing, compilation, assembly, linking and other processes for us. Finally, the Android side generates ELF format files and the iOS side generates Mach-O files. There are four types of ELF format files, including relocatable files (Relocatable File), executable files (Executable File), shared object files (Shared Object File), and core dump files (Core Dump File). Among them, the shared object file, that is, the xxx.so file, contains code and data that can be linked in two contexts. The link editor can process it together with other relocatable files and shared object files to generate another object file; in addition, the dynamic linker (Dynamic Linker) may combine it with an executable file and other shared objects to create a process image. The special effects side supports Douyin's special effects shooting capabilities in the form of a shared object file (libeffect.so).

Since ELF files are involved in program linking and execution, there are usually two views: one is the link view and the other is the execution view (the left figure below); the compiler and linker will form a collection of sections according to the link view, with sections as units, and the section header table (section header table); the loader will follow the execution view, with the file as a collection of segments according to the program header table (program header table) in units of segments. Usually, a relocatable file (xxx.o) will contain a section header table, an executable file (xxx.exe) will contain a program header table, and a shared object file (xxx.so) will contain both.

The following is the section information in effect_sdk.so viewed using the binutils tool:

 $ greatelf -h libeffect_sdk.so
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
Entry point address: 0x0
Start of program headers: 64 (bytes into file)
Start of section headers: 22954168 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 8
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
$ greatelf -S libeffect_sdk.so
There are 29 section headers, starting at offset 0x15e40b8:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.androi[...] NOTE 0000000000000200 00000200
0000000000000098 0000000000000000 A 0 0 4
[2] .note.gnu.bu[...] NOTE 0000000000000298 00000298
0000000000000024 0000000000000000 A 0 0 4
[3] .dynsym DYNSYM 00000000000002c0 000002c0
00000000000107e8 0000000000000018 A 4 1 8
[4] .dynstr STRTAB 0000000000010aa8 00010aa8
000000000001b0f9 0000000000000000 A 0 0 1
[5] .gnu.hash GNU_HASH 000000000002bba8 0002bba8
000000000000347c 0000000000000000 A 3 0 8
[6] .hash HASH 000000000002f028 0002f028
0000000000004c18 0000000000000004 A 3 0 8
... ...
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)

Usually each section is responsible for different functions and is stored in different locations. The size of the section is a reflection of the size of the compiled code. In the final analysis, the final package size of the special effects side is determined by the size of the section and headers. Optimizing the package size means optimizing the code writing efficiency and compilation method, and reducing the size of each section.

 int gInitVar = 24; //-- .data section
int gUninitedVar; //-- .bss section
void func(int i)
{
printf("%d\n", i); //-- .text section
}
int main(void)
{
static int sVar = 23; //-- .data section
static int sVar1; //-- .bss section
int a = 1;
int b;
func(sVar + sVar1 + a + b); //-- .text section
return 0;
}

3 Package size optimization tips

After understanding the basic composition of package size, we can make targeted adjustments to the compilation options and code to optimize the package size.

Both iOS and Android can optimize the code size by optimizing the compilation options. Here are some commonly used ones.

3.1 Compilation Optimization

3.1.1 Use Oz instead of Os

Compile options

  • Use -Oz instead of -Os
  • Example:
 set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Oz")

3.1.2 Reduce the size of unused code

  • Compile options
  • -ffunction-sections
  • Put each function into its own COMDAT segment (COMDAT segment is an auxiliary segment defined by multiple target files. The role of this segment is to group together logical blocks of code and data that are repeated in multiple compiled modules. COMDAT plays a very important role in the compilation and linking of C++ virtual function tables and templates.)
  • Supports Linux/OS X, not Windows
  • -fdata-sections
  • Enables generation of an elf section for each variable in a source file
  • Example:
  •  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -g")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -g")

Link Options

  • -Wl, --gc-sections (Android)

  • When the compiler chooses to compile a file with -ffunction-sections, -fdata-sections, the size of the static library will increase. At this time, calling -Wl, --gc-sections can eliminate the size of unused code and data in the dead segment.
  • -dead_strip (iOS)

Example:

  •  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections")

3.1.3 Enable link optimization

Compile options

  • -flto Oz

Link Options

  • -O3 -flto

  • lto stands for link-time optimization, which needs to be enabled at the same time when compiling and linking. When compiling, each file will be written into a dedicated section, and when linking, they will be treated as the same unit for conversion and optimization. But there is a disadvantage that it will slow down the compilation speed to a certain extent.

  • Note: lto can coexist with -Oz when compiling, but can only coexist with O1/O2/O3 when linking, and cannot coexist with Oz/Os. If both are enabled at the same time, the following error will be reported:

  •  $ clang -Os -fuse-ld=lld -flto test.c
    ld.lld: error: -plugin-opt=Os: number expected, but got 's'
    clang-9: error: linker command failed with exit code 1 (use -v to see invocation)
    $ clang -Oz -fuse-ld=lld -flto test.c
    ld.lld: error: -plugin-opt=Oz: number expected, but got 'z'
    clang-9: error: linker command failed with exit code 1 (use -v to see invocation)

Example:

  •  if (NOT DEFINED ENV{DISABLE_LTO})
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto -fPIC")
    endif()
  •  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl, --gc-sections -fuse-ld=gold -Wl, --icf=safe -O2 -flto")
    if (NOT DEFINED ENV{DISABLE_LTO})
    message(STATUS "DISABLE_LTO=$ENV{DISABLE_LTO} +++ LTO enabled")
    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=gold -Wl, --icf=safe -O2 -flto")
    else()
    message(STATUS "DISABLE_LTO=$ENV{DISABLE_LTO} +++ LTO disabled")
    endif()

3.1.4 Turn off exceptions and rtti

  • Compile options
  • -fno-exceptions
  • When the -fno-rtti switch is turned on, the rtti mechanism will be disabled, reducing the package size.
  • -fno-rtti
  • When the -fno-exceptions switch is turned on, the exception mechanism will be disabled, reducing the package size.
  • The above two methods are relatively radical and require code coordination, but they can also significantly optimize the package size while ensuring the correctness and stability of the code. At present, the special effects side has tried to avoid unnecessary rtti and exception mechanisms.
  • Note: Lack of exception handling and rtti requires coders to write higher quality code.
  • -fno-excpetion requires certain code modifications:
  •  if(!running)
    {
    // throw std::runtime_error("runtime error") // Not available
    errCode = getRuntimeError();
    return errCode;
    }
  • -fno-rtti also requires some code modifications:
  •  DerivedTarget &target = getTargetPtr();
    // dynamic_cast<BasicTarget *>(target.get())->fun(); // Can no longer be used
    static_cast<BasicTarget *>(target.get())->fun();

3.1.5 Automatically remove symbols from imported static libraries

  • Link Options
  • -Wl,--exclude-libs,ALL (Android)
  • Delete automatically exported symbols in library "ALL" (replace ALL with the name of the library you don't need, e.g. --exclude-libs lib,lib,...)
  • Note: iOS does not support this link option, as macOS uses --exclude-libs as the default option.

(If iOS wants to import symbols into the library, you need to manually enable the -reexport-l$(UR_LIB) option)

 if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release" AND ANDROID)
foreach(LIB ${LINK_LIB_LIST})
set(CMAKE_SHARED_LINKER_FLAGS "{CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,lib{LIB}.a")
endforeach()
endif()

Currently, special effects on Android use this option.

3.1.6 Reducing the Symbol Table

  • -fvisibility=hidden
  • The visibility of symbols can be hidden to prevent symbol conflicts and reduce package size.
  • Note: When an error occurs, the upper layer may not be able to locate the problem immediately
 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -g")
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -g")

Currently, the special effects side uses -fvisibility=hidden

3.1.7 Dynamic Linking C++

Dynamically link the libstdc++ library to avoid increasing the size of the library file.

3.2 Code Optimization

In a nutshell: the less code there is, the smaller the package size will be. From experience, 100 lines of code will take up about 1~5K of space. If the ratio exceeds this, there must be something wrong with the code.

3.2.1 Do not have invalid judgment logic (if...else...)

You can use a table-driven approach to implement if else and reduce unnecessary code references.

3.2.2 Reduce template expansion and macro expansion

Template expansion takes up a lot of space, especially for the same form of code, the template will expand into multiple different classes. In this case, it is best to extract the common part and declare it as a static method.

The following is the method of binding variables:

 template <typename T>
static void bindArgs(const Demo& d, T func)
{
auto m = createFun(func);
m->mName = d.name
for (auto i = 0; i < m->getArgc(); ++i)
{
if (i < d.args.size())
m->mArgTypes[i].name = d.args[i];
}
}

template <typename T>
static void bindArgs(const Demo& d, T func, const Var& arg1)
{
auto m = createFun(func);
if (!m)
return;
m->mValues.push_back(arg1);
for (auto i = 0; i < m->getArgc(); ++i)
{
if (i < d.args.size())
m->mArgTypes[i].name = d.args[i];
}
}

// static void bindArgs(const Demo& d, T func, const Var& arg1, const Var& arg2)
// {

Can be modified to:

 // bindArgs extracted
static void bindArgs(const Demo& d, Fun* m)
{
for (auto i = 0; i < m->getArgc(); ++i)
{
if (i < d.args.size())
m->mArgTypes[i].name = d.args[i];
}
}

template <typename T>
static void bindArgs(const Demo& d, T func)
{
auto m = createFun(func);
m->mName = d.name;
bindArgs(d, m);
}

template <typename T>
static void bindArgs(const Demo& d, T func, const Var& arg1)
{
auto m = createFun(func);
if (!m)
return;
m->mValues.push_back(arg1);
bindArgs(d, m);
}

3.2.3 Avoid unnecessary use of stl/std

For example, some callbacks can use function pointers: std::function<> as a class, which is bound to have a higher size cost than a function pointer such as void * fun;

 // using FunInstantiate = std::function<FunInterface*()>; // no longer used
using FunInstantiate = FunInterface*(*)();

For example, when referencing a constant string, you can use the const char* type to avoid the compiler calling the implicit copy constructor;

 // void DemoClass::fun(const std::string &name, const DemoPtr &demoPtr) // no longer used
void DemoClass::fun(const char* name, const DmoePtr &demoPtr)
{
//...
}

3.2.4 Do not define const or static variables in header files

The const / static variables in the header file will be introduced into the corresponding cpp file, which is equivalent to introducing a long string of constant strings in each .o file.

3.2.5 Avoid large arrays

Large arrays take up space equal to the size of the array.

3.2.6 Reduce unnecessary virtual base classes/virtual functions

 // class Child : virtual public Parent // no longer used
class Child : public Parent
{
//...
}

4 Packet Volume Monitoring Tool

4.1 Why do we need a package volume monitoring tool?

Each version of Douyin will have a lot of new capabilities updated, and each update of each requirement will lead to changes in package size. In order to better monitor the changes in package size, confirm the reasons for package size growth, and improve ROI, a package size monitoring tool is introduced to more intuitively confirm the reasons for package size growth, intercept abnormal growth, output the package size growth size and reasons for package size growth brought by each requirement, give package size alarms in time, locate abnormal incremental cases, slow down package size growth, and promote business optimization.

4.2 How to monitor package volume

The package size monitoring tool currently used by the special effects side comes from Google's open source binary file size analysis tool Bloaty, which is used to analyze binary files (xxx.exe, xxx.bin), shared target files (xxx.so), object files (xxx.o) and static libraries (xxx.a), and supports ELF\Mach-O\WebAssembly formats. It can sort out the volume composition of each part of the file, split the size of each section, and combine the symbol information to infer the package size of each method and source file.

Taking the special effects side libeffect_sdk.so as an example, analyze the component units and source files of the .so file and intercept some output results:

 FILE SIZE
--------------
10.3% 2.25Mi [section .rela.dyn]
7.2% 1.58Mi [section .rodata]
7.2% 1.57Mi Bindings.cpp
3.9% 877Ki [section .data.rel.ro]
2.0% 445Ki [section .text]
1.9% 418Ki [section .gcc_except_table]
1.0% 213Ki base/EffectManager.cpp
0.7% 149Ki bef_info_sticker_api.cpp
0.6% 140Ki base/RenderManager.cpp
0.6% 138Ki Runtime/Engine/Foundation/Bindings.cpp
...

Using the above tools, you can more clearly locate the package size increase caused by each file.

4.2.1 Package volume monitoring tool workflow

The package size monitoring tool is a must-have before the current special effects requirements are put on the car. All requirements will be checked for package size after MR (merge request) is proposed and CI packaging is completed. Only package size increments that meet the expected requirements are allowed to be merged. All package size increments correspond to requirements one by one and are recorded.

4.2.2 Analysis capabilities of package volume monitoring tools

The package size analysis tool supports single file analysis and version iteration comparison analysis.

For single file analysis, since the special effects side is mainly delivered through .so files, after each MR is packaged, the tool will automatically obtain the corresponding .so files and .so.symbol files, analyze the package volume composition and package volume source of the library file, and output the package volume size brought by all method functions, sections, and compilation units (xxx.cpp). After confirming the size, the incremental source module of the package volume is confirmed through keyword matching, and the final package volume profile of each module unit and compilation unit is given.

On the other hand, since the special effects capabilities are always updated and iterated based on the requirements, every time a substantial requirement is submitted, the package size difference between the previous version and the current version will be compared, and the incremental source of each version requirement will be recorded. When the incremental value brought by the version comparison result exceeds the expected value, the communication API will be called to send out an alarm indicating that the package size exceeds the limit.

4.2.3 Package volume data record book

All required package size increments will be recorded in the package size log book: when the service receives a demand event, it will call the bits/meego interface to request the demand information and the package size preset exp_pack_size increment to be written into the mr_pkg_size table; after the local package is completed, the actual package size increment real_pack_size will be recorded in the mr_pkg_size table, and the expected value will be compared with the actual increment.

Finally, all package size increments and historical sources of demand increments are recorded, and through the table query interface, the source of package size growth can be identified on the web page based on conditions such as demand name/time period/branch name/commit id.

5 Conclusion

Through the above-mentioned trinity of code volume optimization accumulation, real-time volume monitoring, and the implementation of incremental demand to people, the volume of special effects side packages can be controlled to grow in an orderly manner and code efficiency can be improved.

<<:  Let you know the development history of speech recognition technology

>>:  Spring Festival Event- Technical Solution for Peak Reward Distribution

Recommend

Top sliding menu FDSlideBar

Source code introduction: FDSlideBar is a top sli...

Want to do brand marketing without a prototype? Stop kidding!

As a brand, if you want to enter the hearts of us...

Let’s talk about user churn analysis in the simplest way

Churn analysis is not about analyzing the problem...

Netflix has released 4 original movies in 9 months

In the field of copyrighted videos, Netflix, the ...