About Me
Welcome to my blog! This is where I collect my observations and notes on programming and technology. The main subjects range from implementation details to broader ideas about programming.
Main Topics
- Engineering Projects: Exploring implementation details and how technical systems work.
- C/C++: Notes on language features and programming techniques.
- The Programmer’s Perspective: Ideas about developing a career and a way of thinking as a programmer.
For more, visit the categories page.
Contact
If you have questions or would like to discuss something, please get in touch through the About page.
Thank you for reading and for your support. I hope these notes help you on your own technical journey!
From long multiplication to hardware
Binary multiplication follows the paper-and-pencil method. Each multiplier bit selects either a shifted copy of the multiplicand or zero, and the partial products are added. For example:
1010₂ = 1×2³ + 0×2² + 1×2¹ + 0×2⁰
1010₂ × 1001₂ = 1×1010₂×2⁰ + 0×1010₂×2¹ + 0×1010₂×2² + 1×1010₂×2³
A basic design uses an adder, shifting hardware, and logic to select the partial product. A deliberately simple iterative controller might separate selection, addition, and shifting into several cycles. Combining appropriate operations permits one iteration per cycle. Exact cycle counts depend on the implementation; the central limitation is a sequence proportional to operand width.
More hardware allows partial products to be reduced in parallel. For four terms, two adders can form pairwise sums and another combine their results. This creates a tree with logarithmic depth rather than a linear chain. Wider operands offer greater opportunity, and tree stages can be pipelined. This explanation assumes partial products are already available; it is not a complete timing model for a practical multiplier.
Basic division
Long division proceeds from the most significant side, comparing and subtracting to produce quotient bits. I call the evolving intermediate value a provisional remainder; at the end it becomes the actual remainder.
Unlike independent multiplication partial products, each basic division step depends on the preceding remainder. Faster division is therefore less straightforward. Higher-radix methods, including SRT, select several quotient bits at a time using a more involved algorithm; the original notes leave the details for later study.
One simple signed implementation records operand signs, performs unsigned division, then adjusts the results. The quotient sign depends on both operands. A nonzero remainder follows the dividend’s sign for truncation toward zero.
RISC-V division edge cases
The original notes ask how division could overflow when operands and results have the same width. Signed minimum integer divided by -1 is the exceptional case: the positive mathematical result does not fit. RISC-V integer division specifies results for that case and division by zero rather than raising an arithmetic trap. Floating-point exception flags in fcsr are a separate mechanism, not a way to inspect integer division status. See the RISC-V M extension.
The smallest normal and subnormal binary32 values
| Exponent field | Fraction | Interpretation |
|---|---|---|
| 0 | 0 | Signed zero |
| 0 | Nonzero | Subnormal |
| 1–254 | Any | Normal |
| 255 | 0 | Infinity |
| 255 | Nonzero | NaN |
The smallest positive normal value has exponent field 00000001 and fraction zero, giving 2^-126. The smallest positive subnormal has exponent field zero and only the least significant fraction bit set.
The original study question obtained 2^-150 by applying the normal exponent rule e - 127 when e = 0. That is precisely the special case: subnormals use exponent 1 - 127 = -126 and an implicit leading zero. Thus the minimum is 2^-126 × 2^-23 = 2^-149. Its bit pattern is 0x00000001; changing the exponent field to one would make it a normal number. An all-zero magnitude encodes zero, not 1.0 × 2^-127. These rules resolve the discrepancy raised in the Chinese notes. See the binary32 format explanation.
The progressively smaller fraction gives gradual underflow, filling the interval toward zero with reduced relative precision.
Converting decimal fractions
One useful approach is to write the value as a fraction, reduce it, and inspect its denominator. A finite binary fraction requires the reduced denominator to be a power of two. Convert the numerator to binary and position the radix point accordingly. Splitting a decimal expression into simpler terms can help, but many decimal fractions have repeating binary expansions.
Sign, exponent, and fraction
Floating-point encoding stores a sign and magnitude-like fields rather than a two’s-complement representation of the whole number. The sign, exponent, and fraction arrangement supports efficient ordering for ordinary values, with special handling for negative values, zeros, and NaNs.
The exponent uses a bias. For normal binary32 values, stored exponent = actual exponent + 127. Biasing puts the signed exponent range into an unsigned field. Exponent zero and all ones have the special meanings described above.
Floating-point arithmetic stages
Addition and subtraction align exponents, combine significands according to signs, normalize, round, check exceptional conditions, and write the result. Multiplication combines exponents, multiplies significands, determines the sign, and performs normalization and rounding.
The original notes suggest converting to two’s complement for addition/subtraction. That is one way to reason about an internal signed operation, not a required implementation for all floating-point units.
Integer and floating-point comparisons
RISC-V integer conditional branches combine comparison and branching. The original claim that the base integer ISA has no direct comparison result needs qualification: instructions such as slt and sltu do produce integer comparison results.
F/D comparisons such as feq.s, feq.d, flt.s, flt.d, fle.s, and fle.d write an integer register with zero or one. An integer branch can then test that result. Separate floating-point branch instructions are unnecessary for this sequence.
Floating-point registers and transfer instructions
The conventional F/D extensions add f0 through f31 and transfers such as flw, fsw, fld, and fsd. Address bases remain in integer registers. Unlike x0, f0 is not hardwired to zero. The familiar instruction encoding structure is retained for arithmetic, loads, and stores.
Why have a separate register set when integer and floating-point operands may have the same width? Programs often operate on distinct integer and floating-point data. Separate files increase available registers without widening the five-bit register fields, can provide separate bandwidth, and allow specialized implementation. They also require transfer paths and dedicated instructions, so the hardware and software cost is not literally zero. Some processors internally use representations different from their memory formats.
Another route to fast division
Newton-style iteration can approximate 1/c using a fast multiplier, then multiply that reciprocal by the other operand. Accuracy and final rounding require additional care. This is another approach beyond digit-recurrence division.
C and Java multidimensional arrays
C multidimensional arrays use contiguous row-major layout. Fortran’s column-major layout may favor column-oriented access, but the efficient choice depends on traversal order and the algorithm.
Java represents multidimensional arrays as arrays of arrays, allowing rows of different lengths. This provides flexibility but adds indirection and may require bounds checks; optimizers can eliminate some checks. The performance distinction concerns layout and access patterns as well as language semantics.
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !