Why does setting up some GPIOs cost so much memory?
I think I have found two reasons: 

1. The compiler isn't smart enough to recognize some compile time constants

The problem lies in these macros: #define LCD_XXX_POS  POSITION_VAL(LCD_XXX)

I want to create an automatic mask for various GPIO configuration register. In order to do so, I must at least know the position of each pin. So I use POSITION_VAL(), a macro function provided in ST's CMSIS header :

#define POSITION_VAL(VAL)     (__CLZ(__RBIT(VAL))) 


Basically, the macro function takes an parameter (in this case, single-GPIO-pin mask), flips it with rbit instruction, calculates leading zeros with clz instruction and returns leading zeros. By doing so, I can get the position of LCD_XXX pin quite easily.

The compiler seems to replace the macro function with it body everytime, instead of replacing them with constants. I have tested by defining LCD_XXX_POS as constants, the function size reduced to ~380 bytes.

2. Something was wrong with project setting

About the macro POSITION_VAL(VAL), the compiler may not (do not ?) care if it's fed with constants. So, as suggested by this article, I created a new header :

#ifdef POSITION_VAL
#undef POSITION_VAL
#endif

#ifdef __GNUC__
# define POSITION_VAL(VAL) (__builtin_ctz(VAL))
#else
# define POSITION_VAL(VAL) (__CLZ(__RBIT(VAL)))
#endif



  GNU C has __builtin_ctz which accomplishes the same thing but can deduce the value at compile time if appropriate.


But Truestudio highlighted #ifdef __GNUC__ branch as inactive although I was sure that I was using GCC. I tried with a new project template, #ifdef __GNUC__ branch wasn't marked inactive and the function only cost ~180 bytes. 

Clearly, there was something wrong.....