watermelon 发表于 2020-4-2 18:16:04

【C Python Win32ASM】编码问题与速度对比

本帖最后由 watermelon 于 2020-4-4 16:27 编辑

最近老师对我的Python代码中对try/except来判断文件编码的操作产生了疑问,问我这个是什么意思?所以要求我写一个文档来解释一下并且对比一下python和C的速度问题。
1. python编码问题
1.1文件读写方式;
   一般来说,文件读写有两种常见的方式;
   一种是硬盘I/O,一种是文件的内存映射。
其中,文件的硬盘读写是最为常见的文件读写方式,一般程序语言的默认教程中举例均为文件的硬盘读写方式,其优点是编写简单,考虑的因素比较少,缺点是速度太慢,当读写大体积文件的时候容易使程序卡死而进入假死状态
(无响应)。
   第二种文件的内存映射(file mapping)是文件读写的一种高级方式,其不再使用硬盘进而使用内存来进行文件数据的交换。将硬盘上文件的数据映射到内存中,由于内存的数据传输速度比硬盘的数据传输速度要高很多,所以文件的内存映射会非常的快,处理大数据文件不需要考虑速度问题,但是需要考虑其他的一些问题:如Windows系统上的内存分页,内存分配机制的问题。内存映射的不足之处在于文件映射无法创建新文件(即无法映射空文件);同时文件映射只能修改,编写与源文件中数据长度相等的字符串,所以在添加与源文件长度不相符的内容的时候,应该应用常规的硬盘文件读写方式;最后要注意的一点是文件映射读写的数据是二进制格式的数据,需要对其编码进行判断,转换成我们熟知的常规字符串才可以字符串操作。
1.2 二进制数据与文本数据;
   二进制数据是由二进制字符串组成,如利用记事本打开一个exe文件,会看到由于二进制文件与文本文件互不相通的问题而产生的乱码,此时需要用专门的二进制文件查看工具(如WinHex)来查看二进制文件。
   文本数据是我们常见的可以用记事本打开的文件数据,其字符串是常见的“abcdefg”这类的我们读得懂的字符来显示的。
   总体来说,二进制文件用于计算机的读写,文本文件用于方便人类的读写。
1.3 文件的编码问题
   由于二进制文件与文本文件不是一个类型的文件,所以需要一种转换模式可以帮助二进制文件与文本文件之间自由的转换,而这个转换的规则就是文件的编码。一般来说,我们可能会见到过ASCII格式、UTF-8格式、GBK格式、GB2312格式等类型的编码。
   文本文件给计算机传递信息,计算机只知道01010101二进制,所以需要将我们的abcedf编码(encode)成0101010给计算机认识;当我们想知道计算机干什么的时候,由于计算机是01010101,所以需要将00101010101的二进制数据解码(decode)成常见的字符串abcdef来给我们读写。
   由于python的文件映射出来的数据为二进制,并且如果编码/解码格式选取不正确可能会引发异常,所以可能需要异常处理结构来处理编码的问题,这种情况需要具体问题具体分析。
2.CPython与C在文件映射读取方面的速度比较;
   经过编写代码与实验,在循环100次文件映射读取的基础上,CPython代码的耗时为0.024秒左右;使用Windows API编写的在Windows平台上的C语言程序耗时基本为0.054秒,使用Linux上的文件映射mmap编写的程序在编译命令gcc filemap.c –o filemap.o下编译完成后,程序耗时约为0.005秒(比python快了将近5倍)。

实验的程序:
Cpython代码:
# coding:utf-8

# Purpose: Try to compare the speed of C and CPython code.

import time
import mmap
import os
import os.path

# Get the file size,
# if file is empty, then it can not be file-mapped.
def CheckFileEmpty(filename):
    return os.path.getsize(filename)


def MapFile(filename):
    with open(filename, 'rb') as f:
      mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
      bytes_content = mm.read()
      mm.close()
   
    # Decode the bytes string.
    try:
      content = bytes_content.decode('utf-8')
    except:
      content = bytes_content.decode('GB2312')
   
    # Output the file content.
    #print(content)

def main():
   
    # Start time.
    t1 = time.time()
   
    # Begin the 100 cycles.
    for i in range(100):
      # Check the file size.
      if CheckFileEmpty('temp.txt') == 0:
            print('The file size if zero, please choose another file.')
            return
      
      # Begin to file mapping.
      MapFile('temp.txt')
   
    t2 = time.time()
   
    print('Time consuming: ', str(t2-t1)[:9], 's')

if __name__ == '__main__':
    main()

Linux上C语言:

// This program's time cosumption is 0.005s

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <sys/time.h>

#ifndef BUF_SIZE
#define BUF_SIZE 4096
#endif


void FileMap(const char *filename)
{
        // File descriptor.
        int fp;

        // Set the file attributes structure.
        struct stat st;
       
        //Open the file.
        fp = open(filename, O_RDWR);

        if(fp == -1)
        {
                printf("[ * ] Can't open the file...\n");
                printf("[ * ] Maybe the file access is denied...\n");
                return ;
        }


        // Check the file size.
        fstat(fp, &st);
        if(st.st_size == 0)
        {
                printf("[ * ] Can't make the empty file to the file mapping...\n");
                goto label2;
        }

        // Set the pointer of mmap return,
        // which is also the address of the file content.
        char *buffer = (char*)malloc(sizeof(char)*BUF_SIZE);
        // Record the buffer address.
        // because, when use the mmap, buffer address has been changed.
        char *ptr = buffer;

        if(buffer == NULL)
        {
                printf("[ * ] Memory allocate is failed...\n");
                goto label2;
        }

        buffer = mmap(NULL, BUF_SIZE, PROT_READ, MAP_SHARED, fp, 0);
        if(buffer == NULL)
        {
                printf("[ * ] File mapping has error...\n");
                goto label1;
        }

        //printf("[ * ]The file content is :\n%s\n", buffer);

label1:
        free(ptr);
label2:
        close(fp);
        //printf("[ * ] All done!\n");
        return ;
}




int main(int argc, char *argv[])
{
        struct timeval starttime;
        struct timeval endtime;
        double timeuse = 0;
       
        gettimeofday(&starttime, NULL);
        // 100 cycles;
        for(int i = 0;i<100;i++)
                FileMap("hello.txt");
        gettimeofday(&endtime, NULL);

        timeuse = 1000000 * (endtime.tv_sec - starttime.tv_sec) +
                endtime.tv_usec - starttime.tv_usec;

        timeuse = timeuse / 1000000;
        printf("Time use=%lfs\n", timeuse);

        return 0;
}

       


利用Windows API 编写的文件映射读取的C语言程序:
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

#ifndef BUF_SIZE
#define BUF_SIZE 0 // Used to map the whole file.
#endif

// The file mapping can not manipulate the empty file,
// so we should check the file is empty or not.
DWORD CheckFileEmpty(CHAR *filename)
{
        // Initialize the file attribute information structure.
        WIN32_FILE_ATTRIBUTE_DATA ws32 = { 0 };

        BOOL status = GetFileAttributesExA(filename,
                GetFileExInfoStandard,
                (LPVOID)&ws32);
       
        if (status == FALSE)
        {
                printf("GetFileAttributesExA has a error, the code is: %d\n", GetLastError());
                return 0;
        }

        // Return the file size.
        // Because the file size is very small,
        // so, return the low part is OK.
        return ws32.nFileSizeLow;

}


// Use the file mapping to get the content of target file.
VOID FileMapping(CHAR *filename)
{
        // Get the file open handle.
        HANDLE hFile = CreateFileA(filename,
                GENERIC_READ | GENERIC_WRITE,
                0,        // Exclusive this process to manipulate the file.
                NULL,
                OPEN_ALWAYS,
                FILE_ATTRIBUTE_NORMAL,
                NULL);
        if (hFile == INVALID_HANDLE_VALUE)
        {
                printf("CreateFileA has error, the code is: %d\n", GetLastError());
                return;
        }

        // Create a file mapping object.
        HANDLE hFileMap = CreateFileMappingA(hFile,
                NULL,
                PAGE_READWRITE,        // We can read and write to this file map view.
                0,                               
                BUF_SIZE,                // Get the BUF_SIZE length of data.
                filename);
        if (hFileMap == INVALID_HANDLE_VALUE)
        {
                printf("CreateFileMappingA has error, the code is:%d\n", GetLastError());
                goto label2;
        }

        // Begin to get the content of this file mapping.
        LPVOID lpFileMap = MapViewOfFile(hFileMap,
                FILE_MAP_READ,
                0,
                0,
                // This parameter should be the same with CreateFileMappingA
                // which has the specific data length.
                BUF_SIZE);

        if (lpFileMap == NULL)
        {
                printf("MapViewOfFile has error, the code is:%d\n", GetLastError());
                goto label1;
        }

        //printf("The file content:%s\n", lpFileMap);


label1:
        CloseHandle(hFileMap);
        //printf("Close handle: hFileMap.\n");
label2:
        CloseHandle(hFile);
        //printf("Close handle: hFile.\n");
        UnmapViewOfFile((LPCVOID)lpFileMap);
        //printf("Unmap the file: lpFileMap.\n");
        return;
}


int main(int argc, char *argv[])
{
        // Begin to count the program running time.
        LARGE_INTEGER foreTime = { 0 };
        LARGE_INTEGER backTime = { 0 };
        LARGE_INTEGER freq = { 0 };
       
        // Used to set the specific threads have connection to
        // which CPU cores.
        QueryPerformanceFrequency(&freq);
        QueryPerformanceCounter(&foreTime);

        // Cycle the program 100 times.
        for (int i = 0; i < 100; i++)
        {
                DWORD dwFileSize = 0;
                if ((dwFileSize = CheckFileEmpty("test.txt")) == 0)
                {
                        printf("The empty file can not be file-mapped.\n");
                        return 0;
                }

                //printf("The file size is : %d bytes.\n", dwFileSize);

                // Begin the file mapping.
                FileMapping("test.txt");
        }

        QueryPerformanceCounter(&backTime);

        printf("The time cosumption is : %fs\n",
                (FLOAT)(backTime.QuadPart - foreTime.QuadPart) / (FLOAT)freq.QuadPart);

        return 0;
}


实验文本:temp.txt/test.txt/hello.txt
Hello world!

Win32ASM:本程序在获取文件属性上得到了群大佬GodOfHack与QQ小冰GUID666.....的帮助,表示感谢!
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 文件映射 FILE MAPPING
; 程序来自:www.0xAA55.com
; 该程序遵循较为宽松的MIT许可
; Copyright @ watermelon/ZXG/trace-shadow
; 如果有什么不足的地方请联系我:starlight_chou@163.com
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

                .386
                .model        flat, stdcall
                option        casemap:none

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; include 文件
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

include                windows.inc
include                kernel32.inc
includelib        kernel32.lib
include                user32.inc
includelib        user32.lib


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 数据段
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

                .data
szMsgCaption        db        'Information', 0
szFileName        db        'hello.txt', 0
szError                db        'Error', 0
szErrorCode        db        'Error code is: %d', 0
szEmptyFile        db        'This is an empty file!', 0
szFormat        db        '%d μs', 0
; 用于记录程序运行时间的相关变量
dqForeTime        dq        0
dqBackTime        dq        0
dqFreq                dq        0
dqTime                dq        0
dwlm                dd        1000000



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 代码段
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

                .code

; 判断文件是否为空文件
; 返回值:文件的大小
; 注意:本程序在GetFileAttributesEx中的第二个参数上受到了
; 群大佬GodOfHacker与QQ小冰的指导,小弟在此表示感谢。
_CheckEmptyFile        proc        uses ecx
                        LOCAL        @ws32:WIN32_FILE_ATTRIBUTE_DATA
                        LOCAL        @errorCode:dword
                       
                        ; 初始化结构体,全部填零。
                        invoke        RtlZeroMemory, addr @ws32, sizeof WIN32_FILE_ATTRIBUTE_DATA
                       
                        ; 获取文件属性
                        ; 这里第二个参数应该填GetFileExInfoStandard
                        ; 由于masm32中没有这个参数
                        ; 但是由于本身这个参数位于枚举类型GET_FILEEX_INFO_LEVELS中
                        ; 定义如下
                        ; typedef enum _GET_FILEEX_INFO_LEVELS {
                        ;                        GetFileExInfoStandard,
                        ;                        GetFileExMaxInfoLevel
                        ;                } GET_FILEEX_INFO_LEVELS;
                        ; 依据枚举类型中的知识,每一个元素默认从0号开始往后排
                        ; 所以枚举里面的第一个元素为0
                        invoke        GetFileAttributesEx, offset szFileName, \
                                0, \
                                addr @ws32
                       
                        .if        eax == FALSE
                                ; 获取last error
                                call        GetLastError
                                invoke        wsprintf, addr @errorCode, offset szErrorCode, eax
                                invoke        MessageBox, NULL, addr @errorCode, offset szError, MB_OK
                                xor        eax, eax
                                ret
                        .endif
                       
                        ; 由于此处测试的文件不会大于4G,所以就返回低位的数值了。
                        mov        eax, @ws32.nFileSizeLow
        ret

_CheckEmptyFile endp

; 文件映射
; 无返回值
_FileMapping        proc        uses ecx
                LOCAL        @hFile:dword
                LOCAL        @hFileMap:dword
                LOCAL        @errorCode:dword
                ; 存储文件映射读取出来的内容的首地址
                LOCAL         @content_addr:dword
                LOCAL        @mapViewofFile
               
                ; 打开文件
                invoke        CreateFile, offset szFileName, GENERIC_READ or GENERIC_WRITE, \
                        0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL

                mov        @hFile, eax
               
               
                .if        eax == INVALID_HANDLE_VALUE
                @@:
                        ; 获取last error
                        call        GetLastError
                        invoke        wsprintf, addr @errorCode, offset szErrorCode, eax
                        invoke        MessageBox, NULL, addr @errorCode, offset szError, MB_OK
                        xor        eax, eax
                        ret
                .endif
               
                ; 创建一个文件映射对象
                invoke        CreateFileMapping, @hFile, NULL, \
                        PAGE_READWRITE, 0, 0, offset szFileName
               
                mov        @hFileMap, eax
               
                .if        eax == INVALID_HANDLE_VALUE
                        invoke        CloseHandle, @hFile
                        jmp        @B
                .endif
               
                ; 获取文件内容
                invoke        MapViewOfFile, @hFileMap, \
                        FILE_MAP_READ, 0, 0, 0
               
                .if        eax == NULL
                        invoke        CloseHandle, @hFile
                        invoke        CloseHandle, @hFileMap
                        jmp        @B
                .endif
               
                mov        @content_addr, eax
                ; 显示文件映射的内容
                ;invoke        MessageBox, NULL, @content_addr, offset szMsgCaption, MB_OK
               
                ; 关闭句柄
                invoke        CloseHandle, @hFile
                invoke        CloseHandle, @hFileMap
                invoke        UnmapViewOfFile, @mapViewofFile
       
        xor        eax, eax
        ret
_FileMapping endp


; 计时函数
; 返回值:无
_TimeConsumption        proc
                        ; 用于记录输出时间的格式化字符串地址
                        LOCAL        @time_addr:dword
               
                        ; 开始计时
                        invoke        QueryPerformanceFrequency, offset dqFreq
                        invoke        QueryPerformanceCounter, offset dqForeTime

                        ; 循环100次文件映射的过程
                        mov        ecx, 100
                        @@:
                                call        _CheckEmptyFile
                                .if        eax == 0
                                        invoke        MessageBox, NULL, offset szEmptyFile, offset szMsgCaption, MB_OK
                                        ret
                                .else
                                        call        _FileMapping
                                .endif
                        loop        @B

                        ; 结束计时
                        invoke        QueryPerformanceCounter, offset dqBackTime
                       
                        ; 处理浮点数,参考罗云彬《Windows环境下32位汇编语言程序设计》
                        mov        eax, dword ptr dqForeTime
                        mov        edx, dword ptr dqForeTime + 4
                        sub        dword ptr dqBackTime, eax
                        sbb        dword ptr dqBackTime + 4, edx
                       
                        finit
                        fild        dqFreq
                        fild        dqBackTime
                        fimul        dwlm
                        fdivr
                        fistp        dqTime ; μs
                       
                        invoke        wsprintf, addr @time_addr, offset szFormat, dqTime
                        invoke        MessageBox, NULL, addr @time_addr, offset szMsgCaption, MB_OK
               
               
                xor        eax, eax
                ret
_TimeConsumption endp



; 主程序
start:
                call        _TimeConsumption
                invoke        ExitProcess, NULL

end start


运行得到的时间和Win32API写的写语言的release版本时间差不多,也是0.05秒左右


小弟对于程序的优化问题不是特别懂,希望各位大佬对本文看到有什么问题后直接留言批评我!

实验环境:
Windows: visual studio2013, 32位程序
Cpython:version 3.8.1 64位
Linux上:GCC 8.3.0
汇编器:MASM32

smitest 发表于 2020-4-12 14:05:07

这个速度比起来意义不大啊。整个费时的比较才有效。

watermelon 发表于 2020-4-12 18:23:02

smitest 发表于 2020-4-12 14:05
这个速度比起来意义不大啊。整个费时的比较才有效。

哦哦有道理,本身一开始这个任务和文件映射有关,所以就使用了文件映射这种方法,看来需要再增加循环次数或者改用其他的比较形式。

系统消息 发表于 2020-4-15 09:49:55

你说CPython映射文件速度的问题是在Windows下还是Linux下?屁眼不最终也是通过调用系统API来实现的吗?真的会比系统API还快?

watermelon 发表于 2020-4-17 12:02:09

系统消息 发表于 2020-4-15 09:49
你说CPython映射文件速度的问题是在Windows下还是Linux下?屁眼不最终也是通过调用系统API来实现的吗?真的 ...

我的那个cpython程序是在windows平台下的,后来我也反思了我的程序,感觉不是单一变量来试验了....

0xAA55 发表于 2020-4-18 03:01:37

系统消息 发表于 2020-4-15 09:49
你说CPython映射文件速度的问题是在Windows下还是Linux下?屁眼不最终也是通过调用系统API来实现的吗?真的 ...

你需要注意的是,他每个循环里,都要打开一次文件,映射一下,再关闭文件。而且他在Windows下使用的是缓慢的A系API。这个测试没得什么意义,因为他比的不是通过映射来修改文件的速度,而是“映射的速度”。

就好比我GPU上传、下载纹理资源很慢,但进行图形处理的速度很快,可测速者每个循环都上传、下载纹理,然后只绘制一次,就得出结论“GPU没得CPU快”。

watermelon 发表于 2020-4-18 22:17:42

0xAA55 发表于 2020-4-18 03:01
你需要注意的是,他每个循环里,都要打开一次文件,映射一下,再关闭文件。而且他在Windows下使用的是缓 ...

哦哦学到了,如果比文件映射还是要比读写速度;并且原来A系API要慢啊!
页: [1]
查看完整版本: 【C Python Win32ASM】编码问题与速度对比