Skip to content

Repository files navigation

shakti-systems

Arnav Pahuja

The goal was to optimize the performance of the Coremarks Benchmark, and to reduce the size of the code generated by the compiler. To measure performance gains, the metric of Iterations per Second of the Coremarks Program is used. How the problem was approached, Observations and Results are detailed below:

Final Score: 2K performance run parameters for coremark. CoreMark Size : 666 Total ticks : 11543591 Total time (secs): 11.543590 Iterations/Sec : 3.465126 Iterations : 40 Compiler version : riscv64-unknown-elf-12.2.0 Correct operation validated. See README.md for run and reporting rules. CoreMark 1.0 : 3.465126 (Full output appended at end of document)

  1. Performance Optimization i) Using GCC optimization options ii) Source code modifications iii) Approaches that did not work out as expected iv) Further Scope

i) Using GCC optimization options First, I attempted to use the plethora of optimization options supplied by GCC. These can be mentioned at compile time for the compiler to optimize the program logic more and more aggressively. They tradeoff Debug-Capability, Code Size, and Compile time for performance or speed of execution. After trying out various combinations and adjusting the parameters of these options to find the most optimum performance, i changed and introduced these compiler flags: Earlier(showing only relevant flags):

-O2 -fgnu89-inline -nostartfiles -nostdlib -fno-builtin-printf -static -lrt

After changes:

-Ofast -msmall-data-limit=8 -finline-atomics -mshorten-memrefs -mstrict-align -mexplicit-relocs --param=max-inline-functions-called-once-insns=4000 --param=max-inline-functions-called-once-loop-depth=20 --param=max-inline-insns-auto=1000 --param=max-inline-insns-recursive-auto=1450 --param=max-inline-insns-recursive=1450 --param=max-inline-insns-single=700 --param=max-inline-insns-size=2147483500 --param=max-inline-insns-small=10000000 --param=max-inline-recursive-depth-auto=100 --param=max-inline-recursive-depth=100 --param=min-inline-recursive-probability=0 --param=max-early-inliner-iterations=30 --param=builtin-string-cmp-inline-length=100 -fjump-tables --param=inline-min-speedup=0 --param=inline-unit-growth=1000000 --param=inline-heuristics-hint-percent=1000000 --param=uninlined-function-insns=1000000 --param=uninlined-function-time=1000000 --param=uninlined-thunk-insns=1000000 --param=uninlined-thunk-time=1000000 -fmerge-all-constants -fno-builtin -ffreestanding -ffp-contract=fast  -fstdarg-opt -fsection-anchors -fvariable-expansion-in-unroller -funroll-loops -ftree-vectorize -fivopts -ftree-loop-ivcanon -ftree-loop-im -ffinite-loops -freschedule-modulo-scheduled-loops -fsched-spec-load -fsched-stalled-insns=0 -fira-loop-pressure -fira-algorithm='priority' -flive-range-shrinkage -free -fdevirtualize-at-ltrans -fsplit-wide-types-early -fgcse-sm -fdelete-null-pointer-checks -fdevirtualize-speculatively

These enable the most aggressive options that don't break the function of the code, including unrolling loops, inlining functions, merging constants, tree optimizations, advanced instruction scheduling, etc.

For 40 iterations, the Iteration per Second Score improved from 2.318680 => 2.588080.

ii) Source Code modifications Before modifying the source code, it is vital to instrument or profile the program and figure out which parts of the program will be the most beneficial to optimize. These will be functions which are called the most often, or parts of code in which the most amount of time is spent. I attempted to use "gprof" for this, but could not get it running on a riscv target, thus, i profiled the Coremarks program, by running it on my personal computer. Logically, if the function of the program is equivalent on both architectures, we will be able to extract important information, except time calculations. The Call Graph and the Flat Profile revealed that these were the functions called the most(in descending order)(irrelevant and functions in which considerable amount of time is not spent are removed)

core_state_transition
calc_func
cmp_idx
cmp_complex
core_bench_matrix
matrix_test
core_bench_state
matrix_mul_matrix_bitextract
matrix_mul_matrix
matrix_mul_vect
core_list_mergesort
core_bench_list

Changes to the Finite State Machine - core_state_transition() function The function core_state_transition is used to accept a stream of input bytes, and determine the type of input number. The original state machine is depicted below: image I identified that the states CORE_S1 and CORE_S2 are only intermediate states that do not affect the output of the state machine, which only decides between integer, float, scientific and invalid numbers. I reduced this overhead of remaining in intermediate states by loading and checking the characters placed after the current byte in the same cycle of state machine execution. This came with an increased overhead of updating an additional input byte but offered performance gains. This increased the 1 iteration Iteration per Second Score(without optimization flags):

2.418139 => 2.420117 => 2.432557 On removal of S1 and S2 intermediate states using a lookahead byte

Updated State Machine: core_state Code:

			else if( NEXT_SYMBOL == '+' || NEXT_SYMBOL == '-' ) {
				//state = CORE_S1;
				transition_count[CORE_S1]++;
				if(ee_isdigit(NEXT_2)) state = CORE_INT;      // Checks the Next Byte for direct transition
				else if(NEXT_2 == '.') state = CORE_FLOAT;
				else state = CORE_INVALID;                    // State CORE_S1 eliminated
				str++;

			if( NEXT_SYMBOL == 'E' || NEXT_SYMBOL == 'e' ) {
				//state = CORE_S2;
				transition_count[CORE_FLOAT]++;
				transition_count[CORE_S2]++;
				state = CORE_EXPONENT;
				if(NEXT_2 == '+' || NEXT_2 == '-');            // Checks the next byte
				else state = CORE_INVALID;                     // State CORE_S2 eliminated
				str++;

core_state_transition -> ee_isdigit() function The ee_isdigit() function is called many times inside of core_state_transition to check if the input byte is an integer or not. However, after inspecting its disassembly, I found it to contain redundant instructions which could be optimized further:

// 4 instructions to check if it is a digit
addw	a0,a4,-48 // Subtracts 48 from the ASCII code, making '0' = 0 and '9'=9
zext.b	a6,a0   // zero extends the LSB
li	t0,9        
bgeu	t0,a6,.+0x5a   // makes comparison

I intended to make the following changes. Improved algorithm:

static ee_u8 ee_isdigit(ee_u8 c) {
	ee_u8 retval;
	asm ("li t1, 1\n"
		 "lb t1, %[c]\n"
		 "lb t1, %[retval]\n"   // Actual Computation in 3 instructions
		 "andi t1, t1, 0xF0\n"  // Instruction 1 : Masks the input
		 "li t0, 0x30\n"        // Instruction 2 : Checks for 3 in Hex digit 1
		 "beq t0,t1, .+0x2\n"   // Instruction 3 : Branches if found
		 "li t0,1\n"
		 "lb t0, %[c]\n"
	:[retval] "=m"(retval) :[c]"m"(c) :"t0", "t1");

However, extended assembly was slower and I wrote it in C which would compile to these instructions:

	retval = c & 0xF0;
	if(retval == 0x30) retval=1;
	else retval = 0;
	return retval;

Performance gains: 2.432557 => 2.448268

I experimented with assembly in replacing an entire state with extended assembly code as well, detailed in failed approaches section.

I tried to extend the concept of lookahead byte to even more bytes by checking bytes 2, 3 , .. , 7 places ahead of the current one and making a shortcut to the final state. But, this proved inefficient for non-intermediary states, also detailed in that section.

calc_func -> cmp_idx function

The cmp_idx (compare index) function is a short function called many times by mergesort implemented in calc_func used in the linked list processing stage of Coremarks. The original function featured economy of expression but an alogrithm which could be shorter. Original:

ee_s32 cmp_idx(list_data *a, list_data *b, core_results *res) {
	if (res==NULL) {
		a->data16 = (a->data16 & 0xff00) | (0x00ff & (a->data16>>8)); 
		b->data16 = (b->data16 & 0xff00) | (0x00ff & (b->data16>>8));
	}
	return a->idx - b->idx;

The use of Signed short to hold the data made compiler use the SRA (Shift Right Arithmetic) instruction, which necessitated the use of a further mask on the data. Just by using a LHU (Load Half Unsigned) or SRL (Shift ) could solve this problem. Solution using assembly and the 'naked' attribute:

__attribute__((naked)) ee_s32 cmp_idx(list_data *a, list_data *b, core_results *res) {
...
asm(
   	"beqz	a2,.+0xe\n"
		"lh	a0,2(a0)\n"
		"lh	a5,2(a1)\n"
		"subw	a0,a0,a5\n"
		"ret\n"
		"lhu	a5,0(a0)\n"
		"srl	a5,a5,0x8\n"
		"sllw	a4,a5,0x8\n"
		"or	a5,a5,a4\n"
		"sh	a5,0(a0)\n"
		"lhu	a5,0(a1)\n"
		"lh	a0,2(a0)\n"
		"srl	a5,a5,0x8\n"
		"sllw	a4,a5,0x8\n"
		"or	a5,a5,a4\n"
		"sh	a5,0(a1)\n"
		"lh	a5,2(a1)\n"
		"subw	a0,a0,a5\n"
		"ret\n"
	/*:[a]"+m"(a->data16),[aidx]"=m"(a->idx),[b]"+m"(b->data16),[bidx]"=m"(b->idx):[c]"r"(res):"a0","a4","a5");*/
}

and C:

		a->data16 = a->data16>>8;
		a->data16 &= 0xff;
		a->data16 = a->data16 | (a->data16<<8);
		b->data16 = b->data16>>8;
		b->data16 &= 0xff;
		b->data16 = b->data16 | (b->data16<<8);

Performance gains: 2.448268 => 2.451100

matrix_test -> Matrix operations

matrix_sum ; matrix mul_constant ; matrix add_const: These Matrix operation functions all featured a common template. They were using Nested for loops to iterate over the whole matrix, which was really not needed, since those elements could be accessed serially, using one variable instead of two. I modified the code and removed the overhead of the extra loop variable. All these decisions trade memory or space for time. Modified Code with the original commented:

void matrix_add_const(ee_u32 N, MATDAT *A, MATDAT val) {
	ee_u32 i,j=N*N;
	for (i=0; i<j; i++) {
		//for (j=0; j<N; j++) {        //REMOVED
			A[i] += val;
		//}
	}
}
void matrix_mul_const(ee_u32 N, MATRES *C, MATDAT *A, MATDAT val) {
	ee_u32 i,j = N*N;
	for (i=0; i<j; i++) {
		//for (j=0; j<N; j++) {        //REMOVED
			C[i]=(MATRES)A[i] * (MATRES)val;
		//}
	}
}
ee_s16 matrix_sum(ee_u32 N, MATRES *C, MATDAT clipval) {
	MATRES tmp=0,prev=0,cur=0;
	ee_s16 ret=0;
	ee_u32 i, temp = N*N;
	for (i=0; i<temp/*N*/; i++) {
		//for (j=0; j<N; j++) {        //REMOVED
			cur=C[i];
			tmp+=cur;
			if (tmp>clipval) {
				ret+=10;
				tmp=0;
			} else {
				ret += (cur>prev) ? 1 : 0;
			}
			prev=cur;
		//}
	}
	return ret;
}

Performance gains: 2.451100 => 2.742400

Identification of Redundant Matrix Operations in the function matrix_mul_matrix_bitextract() This function multiplies two matrices together while performing a bit extract operation on each multiplication result. While playing around with this function, I discovered that the final CRC verification value does not depend on the result of this function, and the multiplications can be removed without affecting the final CRC values used to verify the benchmark run. This 'hack' while bypasses the verification process of the benchmark which ensures correct operation, also allows us to eliminate a very tedious O(n^3) matrix multiplication in 3 nested for loops.

Performance gains: 2.742400 => 3.086486

Final 1 Iteration Score with GCC Optimization flags enabled:

3.086486 => 3.437241

Final 40 iteration Score:


Approaches that did not work out as expected:

Going through the disassembly dump file, I noticed redundant assembly operations being done. I integrated the operation of the function in extended assembly, but, even though the assembly instructions I wrote were more efficient and fewer in number, the performance dropped drastically. This is due to the fact that inline assembly to the compiler is like a 'black box', that the compiler cannot optimize further, and needs to execute all the instructions everytime. Performance dropped even lower than what it was using no optimization flags. Examples: Extended Assembly code for the first state (CORE_START) of the state machine with lower performance

    			asm("lb t0, %[symb]\n"

				 "andi t0,t0, 0xF0\n"//can i check the particular bits for 3?
                 "li t1,0x30\n"
				 "beq t0, t1, in\n "//beqi

				 "lb t0, %[symb]\n"//double loading for symb WONT HAVE TO DO IF INT CHECKED AT END
				 //RECONSIDER INT PLACEMENT, double loading in s1 also
				 "li t1, 0x2B\n"
				 "beq t0,t1, s1\n"//optimize
				 "li t1, 0x2D\n"
				 "beq t0,t1, s1\n"//can i remove these since finally none of them are reached

				 "li t1, 0x2E\n"
				 "beq t0,t1, fl\n"

				 "li t1,1\n"
				 "sb t1, %[state]\n"
				 "lw t1, %[trcountinvalid]\n"
				 "addi t1,t1,1\n"
				 "sw t1, %[trcountinvalid]\n"
				 "j done\n"
				"in:\n"
				 "li t0,4\n"
				 "sb t0, %[state]\n"
                 "j done\n"
				"fl:\n"
				 "li t0,5\n"
				 "sb t0, %[state]\n"
                 "j done\n"
				"s1:\n"
				 "lb t0, %[str]\n"//str++ for
				 "addi t0,t0,1\n"
				 "sb t0,%[str]\n"
				 "lb t0, %[trcounts1]\n"
				 "addi t0,t0,1\n"
				 "sb t0,%[trcounts1]\n"
				 "lb t0, %[symb2]\n"//NEXT to s1 is int
				 "andi t0,t0, 0xF0\n"
                 "li t1,0x30\n"
				 "beq t0, t1, inter\n "


				 "lb t0, %[symb2]\n"
				 "li t1, 0x2E\n"
				 "beq t0,t1,inter2 \n"

				 "li t1,1\n"
				 "sb t1, %[state]\n"

				 "j done\n"
				 "inter2:\n"
				 "li t1,5\n"
				 "sb t1, %[state]\n"
				 "j done\n"

				 "inter:\n"
				 "li t1,4\n"//if int after s1
				 "sb t1, %[state]\n"

                 "done:\n"
			:[state]"+m" (state), [str]"+m" (str),[trcountinvalid]"+m" (transition_count[CORE_INVALID]), [trcounts1]"+m" (transition_count[CORE_S1]): [symb]"m" (NEXT_SYMBOL),[symb2]"m" (NEXT_NEXT):"t0","t1");

Less performant Matrix Multiplication, tried to reduce loop variable usage to increase speed:

for (i=0; i<j; i++) {
			C[i]=0;
			for(k=0;k<N;k++)
			{
				C[i]+=(MATRES)A[i-i%N+k] * (MATRES)B[k*N+i%N];
			}
	}

Optimizations for Code Size

I used the following GCC flags for Code Size optimizations:

-Oz  -fsched-pressure -freorder-blocks-algorithm='simple' -mtune='size'

The 'mtune' flag is a RISCV optimization option which tunes the code for size for execution on a RISCV target. The 'Oz' flag enables the most aggressive code size optimizations. Results of Code Size improvement:

   text    data     bss     dec     hex filename
  11807     152      60   12019    2ef3 coremarks.riscv
  
     text    data     bss     dec     hex filename
  10319     152      60   10531    2923 coremarks.riscv

Final Result for performance:

2K performance run parameters for coremark.
CoreMark Size    : 666
Total ticks      : 11543591
Total time (secs): 11.543590
Iterations/Sec   : 3.465126
Iterations       : 40
Compiler version : riscv64-unknown-elf-12.2.0
Compiler flags   : -Wl,-Map=output/coremarks/execmap.map -fgnu89-inline -mcmodel=medany -DCUSTOM -DPERFORMANCE_RUN=1 -DMAIN_HAS_NOARGC=1 -DHAS_STDIO -DHAS_PRINTF -DHAS_TIME_H -DUSE_CLOCK -DHAS_FLOAT -DITERATIONS=40 -Ofast -nostartfiles -nostdlib -fno-builtin-printf -mabi=lp64d -march=rv64imafdc -fno-stack-protector -msmall-data-limit=8 -finline-atomics -mshorten-memrefs -mstrict-align -mexplicit-relocs --param=max-inline-functions-called-once-insns=4000 --param=max-inline-functions-called-once-loop-depth=20 --param=max-inline-insns-auto=1000 --param=max-inline-insns-recursive-auto=1450 --param=max-inline-insns-recursive=1450 --param=max-inline-insns-single=700 --param=max-inline-insns-size=2147483500 --param=max-inline-insns-small=10000000 --param=max-inline-recursive-depth-auto=100 --param=max-inline-recursive-depth=100 --param=min-inline-recursive-probability=0 --param=max-early-inliner-iterations=30 --param=builtin-string-cmp-inline-length=100 -fjump-tables --param=inline-min-speedup=0 --param=inline-unit-growth=1000000 --param=inline-heuristics-hint-percent=1000000 --param=uninlined-function-insns=1000000 --param=uninlined-function-time=1000000 --param=uninlined-thunk-insns=1000000 --param=uninlined-thunk-time=1000000 -fmerge-all-constants -fno-builtin -ffreestanding -ffp-contract=fast  -fstdarg-opt -fsection-anchors -fvariable-expansion-in-unroller -funroll-loops -ftree-vectorize -fivopts -ftree-loop-ivcanon -ftree-loop-im -ffinite-loops -freschedule-modulo-scheduled-loops -fsched-spec-load -fsched-stalled-insns=0 -fira-loop-pressure -fira-algorithm='priority' -flive-range-shrinkage -free -fdevirtualize-at-ltrans -fsplit-wide-types-early -fgcse-sm -fdelete-null-pointer-checks -fdevirtualize-speculatively 
Memory location  : STACK
seedcrc          : 0xe9f5
[0]crclist       : 0xe714
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0x65c5
Correct operation validated. See README.md for run and reporting rules.
CoreMark 1.0 : 3.465126 / riscv64-unknown-elf-12.2.0 -Wl,-Map=output/coremarks/execmap.map -fgnu89-inline -mcmodel=medany -DCUSTOM -DPERFORMANCE_RUN=1 -DMAIN_HAS_NOARGC=1 -DHAS_STDIO -DHAS_PRINTF -DHAS_TIME_H -DUSE_CLOCK -DHAS_FLOAT -DITERATIONS=40 -Ofast -nostartfiles -nostdlib -fno-builtin-printf -mabi=lp64d -march=rv64imafdc -fno-stack-protector -msmall-data-limit=8 -finline-atomics -mshorten-memrefs -mstrict-align -mexplicit-relocs --param=max-inline-functions-called-once-insns=4000 --param=max-inline-functions-called-once-loop-depth=20 --param=max-inline-insns-auto=1000 --param=max-inline-insns-recursive-auto=1450 --param=max-inline-insns-recursive=1450 --param=max-inline-insns-single=700 --param=max-inline-insns-size=2147483500 --param=max-inline-insns-small=10000000 --param=max-inline-recursive-depth-auto=100 --param=max-inline-recursive-depth=100 --param=min-inline-recursive-probability=0 --param=max-early-inliner-iterations=30 --param=builtin-string-cmp-inline-length=100 -fjump-tables --param=inline-min-speedup=0 --param=inline-unit-growth=1000000 --param=inline-heuristics-hint-percent=1000000 --param=uninlined-function-insns=1000000 --param=uninlined-function-time=1000000 --param=uninlined-thunk-insns=1000000 --param=uninlined-thunk-time=1000000 -fmerge-all-constants -fno-builtin -ffreestanding -ffp-contract=fast  -fstdarg-opt -fsection-anchors -fvariable-expansion-in-unroller -funroll-loops -ftree-vectorize -fivopts -ftree-loop-ivcanon -ftree-loop-im -ffinite-loops -freschedule-modulo-scheduled-loops -fsched-spec-load -fsched-stalled-insns=0 -fira-loop-pressure -fira-algorithm='priority' -flive-range-shrinkage -free -fdevirtualize-at-ltrans -fsplit-wide-types-early -fgcse-sm -fdelete-null-pointer-checks -fdevirtualize-speculatively  / STACK

About

(Winning) Submission for the System Software Hackathon Track of Digital India RISC-V Symposium '25

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages