For a simple C program that only uses Win32 API (no C++, no CRT, ...), then the generated object and executable code is comparable in size as the equivalent binaries coming from assembler code.
Compare:
#define VC_EXTRALEAN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// For C only (no C++), use this entry point, as WinMain is too big and not required
EXTERN_C int WINAPI WinMainCRTStartup(void) {
int retVal = MessageBoxA(0, "Hello, World!", "64 bit Windows C", 0);
ExitProcess(retVal);
}
against:
bits 64
extern MessageBoxA
extern ExitProcess
section .text
global main
main:
sub rsp, 0x28
mov rcx, 0 ; hWnd = HWND_DESKTOP
mov rdx, qword message ; LPCSTR lpText
mov r8, qword title ; LPCSTR lpCaption
mov r9d, 0 ; uType = MP_OK
call MessageBoxA
add rsp, 0x28
mov ecx, eax ; uExitCode = MessageBox(...)
call ExitProcess
section .data
title: db "64 bit Windows assembler", 0
message: db "Hello, world!", 0
Both C and assembler can be made into EXE files of exactly the same size, running at seemingly the same speed. Only the object files have different sizes (the one from C being 1KiB instead of 0.5Kib from assembler).
To make this happen, specific compiler and linker options must be set. Using a cygwin Makefile for GNU make:
PATH := /cygdrive/c/Program Files (x86)/Microsoft Visual Studio 11.0/VC/BIN/amd64:${PATH}
VCINCLUDE = "c:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\INCLUDE"
SDKINCLUDE = "c:\Program Files (x86)\Windows Kits\8.0\Include\um"
SDKSHAREDINCLUDE = "c:\Program Files (x86)\Windows Kits\8.0\Include\shared"
VCLIBPATH = "c:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\LIB\amd64"
SDKLIBPATH = "c:\Program Files (x86)\Windows Kits\8.0\Lib\win8\um\x64"
CC = cl.exe
COPTS = /nologo /TC /favor:INTEL64 /MT /GA /GR- /Ox /w /Y- /I ${VCINCLUDE} /I ${SDKINCLUDE} /I ${SDKSHAREDINCLUDE}
LD = link.exe
LDOPTS = /nologo /MACHINE:X64 /OPT:REF /OPT:ICF /nodefaultlib /SUBSYSTEM:WINDOWS
LDOBJS = /LIBPATH:${VCLIBPATH} /LIBPATH:${SDKLIBPATH} kernel32.lib user32.lib
ASM = /cygdrive/c/nasm/nasm.exe
ASMOPTS = -f win64 -Ox
all: test1.exe test2.exe
test1.exe: test1.obj
${LD} ${LDOPTS} /entry:main /OUT:test1.exe test1.obj ${LDOBJS}
test1.obj: test1.asm
${ASM} ${ASMOPTS} -o test1.obj test1.asm
test2.obj: test2.c
$(CC) ${COPTS} /c test2.c
test2.exe: test2.obj
${LD} ${LDOPTS} /OUT:test2.exe test2.obj ${LDOBJS}